Consenti l'accesso all'utente

Questa è la seconda procedura dettagliata della serie di procedure dettagliate sui componenti aggiuntivi di Classroom.

In questa procedura dettagliata, aggiungerai Accedi con Google all'applicazione web. Questo è un comportamento obbligatorio per i componenti aggiuntivi di Classroom. Utilizza le credenziali di questo flusso di autorizzazione per tutte le chiamate future all'API.

Nel corso di questa procedura dettagliata, eseguirai le seguenti operazioni:

  • Configura la tua app web in modo da gestire i dati delle sessioni all'interno di un iframe.
  • Implementa il flusso di accesso server-to-server di Google OAuth 2.0.
  • Effettua una chiamata all'API OAuth 2.0.
  • Crea route aggiuntive per supportare l'autorizzazione, la disconnessione e il test delle chiamate API.

Al termine, puoi autorizzare completamente gli utenti nella tua app web ed emettere chiamate alle API di Google.

Informazioni sul flusso di autorizzazione

Le API di Google utilizzano il protocollo OAuth 2.0 per l'autenticazione e l'autorizzazione. La descrizione completa dell'implementazione OAuth di Google è disponibile nella guida OAuth di Google Identity.

Le credenziali della tua applicazione sono gestite in Google Cloud. Dopo aver creato questi elementi, implementa una procedura di quattro passaggi per autorizzare e autenticare un utente:

  1. Richiedi l'autorizzazione. Fornisci un URL di callback come parte di questa richiesta. Al termine, riceverai un URL di autorizzazione.
  2. Reindirizza l'utente all'URL di autorizzazione. La pagina visualizzata informa l'utente delle autorizzazioni richieste dalla tua app e gli chiede di consentire l'accesso. Al termine, l'utente viene indirizzato all'URL di callback.
  3. Ricevi un codice di autorizzazione sul percorso di callback. Scambia il codice di autorizzazione con un token di accesso e un token di aggiornamento.
  4. Effettuare chiamate a un'API di Google utilizzando i token.

Ottenere le credenziali OAuth 2.0

Assicurati di aver creato e scaricato le credenziali OAuth come descritto nella pagina Panoramica. Il tuo progetto deve utilizzare queste credenziali per accedere all'utente.

Implementare il flusso di autorizzazione

Aggiungi logica e percorsi alla nostra app web per realizzare il flusso descritto sopra, incluse le seguenti funzionalità:

  • Avvia il flusso di autorizzazione al raggiungimento della pagina di destinazione.
  • Richiedi l'autorizzazione e gestisci la risposta del server di autorizzazione.
  • Cancella le credenziali archiviate.
  • Revoca le autorizzazioni dell'app.
  • Testare una chiamata API.

Avvia autorizzazione

Modifica la pagina di destinazione per avviare il flusso di autorizzazione, se necessario. Il componente aggiuntivo può essere in due possibili stati: se sono presenti token salvati nella sessione corrente oppure devi ottenere i token dal server OAuth 2.0. Esegui una chiamata API di prova se sono presenti token nella sessione o richiedi in altro modo all'utente di accedere.

Python

Apri il file routes.py. Innanzitutto, imposta un paio di costanti e la nostra configurazione dei cookie in base ai suggerimenti sulla sicurezza dell'iframe.

# The file that contains the OAuth 2.0 client_id and client_secret.
CLIENT_SECRETS_FILE = "client_secret.json"

# The OAuth 2.0 access scopes to request.
# These scopes must match the scopes in your Google Cloud project's OAuth Consent
# Screen: https://console.cloud.google.com/apis/credentials/consent
SCOPES = [
    "openid",
    "https://www.googleapis.com/auth/userinfo.profile",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/classroom.addons.teacher",
    "https://www.googleapis.com/auth/classroom.addons.student"
]

# Flask cookie configurations.
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="None",
)

Passa alla route di destinazione del componente aggiuntivo (/classroom-addon nel file di esempio). Aggiungi logica per visualizzare una pagina di accesso se la sessione non contiene la chiave "credenziali".

@app.route("/classroom-addon")
def classroom_addon():
    if "credentials" not in flask.session:
        return flask.render_template("authorization.html")

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

Java

Il codice per questa procedura dettagliata si trova nel modulo step_02_sign_in.

Apri il file application.properties e aggiungi la configurazione della sessione che segue i consigli sulla sicurezza dell'iframe.

# iFrame security recommendations call for cookies to have the HttpOnly and
# secure attribute set
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

# Ensures that the session is maintained across the iframe and sign-in pop-up.
server.servlet.session.cookie.same-site=none

Crea una classe di servizio (AuthService.java nel modulo step_02_sign_in) per gestire la logica alla base degli endpoint nel file del controller e configura l'URI di reindirizzamento, la posizione del file dei client secret e gli ambiti richiesti dal componente aggiuntivo. L'URI di reindirizzamento viene utilizzato per reindirizzare gli utenti a un URI specifico dopo che hanno autorizzato l'app. Consulta la sezione relativa alla configurazione del progetto di README.md nel codice sorgente per informazioni su dove posizionare il file client_secret.json.

@Service
public class AuthService {
    private static final String REDIRECT_URI = "https://localhost:5000/callback";
    private static final String CLIENT_SECRET_FILE = "client_secret.json";
    private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
    private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();

    private static final String[] REQUIRED_SCOPES = {
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/userinfo.email",
        "https://www.googleapis.com/auth/classroom.addons.teacher",
        "https://www.googleapis.com/auth/classroom.addons.student"
    };

    /** Creates and returns a Collection object with all requested scopes.
    *   @return Collection of scopes requested by the application.
    */
    public static Collection<String> getScopes() {
        return new ArrayList<>(Arrays.asList(REQUIRED_SCOPES));
    }
}

Apri il file controller (AuthController.java nel modulo step_02_sign_in) e aggiungi logica alla route di destinazione per visualizzare la pagina di accesso se la sessione non contiene la chiave credentials.

@GetMapping(value = {"/start-auth-flow"})
public String startAuthFlow(Model model) {
    try {
        return "authorization";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

@GetMapping(value = {"/addon-discovery"})
public String addon_discovery(HttpSession session, Model model) {
    try {
        if (session == null || session.getAttribute("credentials") == null) {
            return startAuthFlow(model);
        }
        return "addon-discovery";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

La pagina di autorizzazione deve contenere un link o un pulsante che consente all'utente di "accedere". Se fa clic su questa opzione, l'utente verrà reindirizzato alla route authorize.

Richiedi autorizzazione

Per richiedere l'autorizzazione, crea e reindirizza l'utente a un URL di autenticazione. Questo URL include diverse informazioni, ad esempio gli ambiti richiesti, la route di destinazione per dopo l'autorizzazione e l'ID client dell'app web. Puoi vederli in questo URL di autorizzazione di esempio.

Python

Aggiungi la seguente importazione al tuo file routes.py.

import google_auth_oauthlib.flow

Crea una nuova route /authorize. Crea un'istanza di google_auth_oauthlib.flow.Flow. Per farlo, ti consigliamo vivamente di utilizzare il metodo from_client_secrets_file incluso.

@app.route("/authorize")
def authorize():
    # Create flow instance to manage the OAuth 2.0 Authorization Grant Flow
    # steps.
    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES)

Imposta il valore redirect_uri di flow; si tratta della route a cui vuoi che gli utenti restino indietro dopo aver autorizzato la tua app. Questo valore è /callback nell'esempio seguente.

# The URI created here must exactly match one of the authorized redirect
# URIs for the OAuth 2.0 client, which you configured in the API Console. If
# this value doesn't match an authorized URI, you will get a
# "redirect_uri_mismatch" error.
flow.redirect_uri = flask.url_for("callback", _external=True)

Utilizza l'oggetto flow per creare authorization_url e state. Archivia state nella sessione; viene utilizzato per verificare in seguito l'autenticità della risposta del server. Infine, reindirizza l'utente a 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")

# Store the state so the callback can verify the auth server response.
flask.session["state"] = state

# Redirect the user to the OAuth authorization URL.
return flask.redirect(authorization_url)

Java

Aggiungi i seguenti metodi al file AuthService.java per creare un'istanza dell'oggetto flusso, quindi utilizzalo per recuperare l'URL di autorizzazione:

  • Il metodo getClientSecrets() legge il file client secret e crea un oggetto GoogleClientSecrets.
  • Il metodo getFlow() crea un'istanza di GoogleAuthorizationCodeFlow.
  • Il metodo authorize() utilizza l'oggetto GoogleAuthorizationCodeFlow, il parametro state e l'URI di reindirizzamento per recuperare l'URL di autorizzazione. Il parametro state viene utilizzato per verificare l'autenticità della risposta dal server di autorizzazione. Il metodo restituisce quindi una mappa con l'URL di autorizzazione e il parametro state.
/** Reads the client secret file downloaded from Google Cloud.
 *   @return GoogleClientSecrets read in from client secret file. */
public GoogleClientSecrets getClientSecrets() throws Exception {
    try {
        InputStream in = SignInApplication.class.getClassLoader()
            .getResourceAsStream(CLIENT_SECRET_FILE);
        if (in == null) {
            throw new FileNotFoundException("Client secret file not found: "
                +   CLIENT_SECRET_FILE);
        }
        GoogleClientSecrets clientSecrets = GoogleClientSecrets
            .load(JSON_FACTORY, new InputStreamReader(in));
        return clientSecrets;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns authorization code flow.
*   @return GoogleAuthorizationCodeFlow object used to retrieve an access
*   token and refresh token for the application.
*   @throws Exception if reading client secrets or building code flow object
*   is unsuccessful.
*/
public GoogleAuthorizationCodeFlow getFlow() throws Exception {
    try {
        GoogleAuthorizationCodeFlow authorizationCodeFlow =
            new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT,
                JSON_FACTORY,
                getClientSecrets(),
                getScopes())
                .setAccessType("offline")
                .build();
        return authorizationCodeFlow;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns a map with the authorization URL, which allows the
*   user to give the app permission to their account, and the state parameter,
*   which is used to prevent cross site request forgery.
*   @return map with authorization URL and state parameter.
*   @throws Exception if building the authorization URL is unsuccessful.
*/
public HashMap authorize() throws Exception {
    HashMap<String, String> authDataMap = new HashMap<>();
    try {
        String state = new BigInteger(130, new SecureRandom()).toString(32);
        authDataMap.put("state", state);

        GoogleAuthorizationCodeFlow flow = getFlow();
        String authUrl = flow
            .newAuthorizationUrl()
            .setState(state)
            .setRedirectUri(REDIRECT_URI)
            .build();
        String url = authUrl;
        authDataMap.put("url", url);

        return authDataMap;
    } catch (Exception e) {
        throw e;
    }
}

Utilizza l'inserimento del costruttore per creare un'istanza della classe di servizio nella classe controller.

/** Declare AuthService to be used in the Controller class constructor. */
private final AuthService authService;

/** AuthController constructor. Uses constructor injection to instantiate
*   the AuthService and UserRepository classes.
*   @param authService the service class that handles the implementation logic
*   of requests.
*/
public AuthController(AuthService authService) {
    this.authService = authService;
}

Aggiungi l'endpoint /authorize alla classe del controller. Questo endpoint chiama il metodo authorize() di AuthService per recuperare il parametro state e l'URL di autorizzazione. Successivamente, l'endpoint memorizza il parametro state nella sessione e reindirizza gli utenti all'URL di autorizzazione.

/** Redirects the sign-in pop-up to the authorization URL.
*   @param response the current response to pass information to.
*   @param session the current session.
*   @throws Exception if redirection to the authorization URL is unsuccessful.
*/
@GetMapping(value = {"/authorize"})
public void authorize(HttpServletResponse response, HttpSession session)
    throws Exception {
    try {
        HashMap authDataMap = authService.authorize();
        String authUrl = authDataMap.get("url").toString();
        String state = authDataMap.get("state").toString();
        session.setAttribute("state", state);
        response.sendRedirect(authUrl);
    } catch (Exception e) {
        throw e;
    }
}

Gestire la risposta del server

Dopo l'autorizzazione, l'utente torna alla route redirect_uri dal passaggio precedente. Nell'esempio precedente, il percorso è /callback.

Ricevi un code nella risposta quando l'utente torna dalla pagina di autorizzazione. Quindi scambia il codice con i token di accesso e di aggiornamento:

Python

Aggiungi le seguenti importazioni al file del server Flask.

import google.oauth2.credentials
import googleapiclient.discovery

Aggiungi il percorso al server. Crea un'altra istanza di google_auth_oauthlib.flow.Flow, ma questa volta riutilizza lo stato salvato nel passaggio precedente.

@app.route("/callback")
def callback():
    state = flask.session["state"]

    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES, state=state)
    flow.redirect_uri = flask.url_for("callback", _external=True)

Quindi, richiedi i token di accesso e di aggiornamento. Fortunatamente, l'oggetto flow contiene anche il metodo fetch_token per ottenere questo risultato. Il metodo prevede gli argomenti code o authorization_response. Utilizza authorization_response, poiché è l'URL completo della richiesta.

authorization_response = flask.request.url
flow.fetch_token(authorization_response=authorization_response)

Ora hai le credenziali complete. Archiviali nella sessione in modo che possano essere recuperati in altri metodi o percorsi, poi reindirizza a una pagina di destinazione del componente aggiuntivo.

credentials = flow.credentials
flask.session["credentials"] = {
    "token": credentials.token,
    "refresh_token": credentials.refresh_token,
    "token_uri": credentials.token_uri,
    "client_id": credentials.client_id,
    "client_secret": credentials.client_secret,
    "scopes": credentials.scopes
}

# Close the pop-up by rendering an HTML page with a script that redirects
# the owner and closes itself. This can be done with a bit of JavaScript:
# <script>
#     window.opener.location.href = "{{ url_for('classroom_addon') }}";
#     window.close();
# </script>
return flask.render_template("close-me.html")

Java

Aggiungi alla tua classe di servizio un metodo che restituisca l'oggetto Credentials trasmettendo il codice di autorizzazione recuperato dal reindirizzamento eseguito dall'URL di autorizzazione. Questo oggetto Credentials viene utilizzato in un secondo momento per recuperare il token di accesso e aggiornare il token.

/** Returns the required credentials to access Google APIs.
*   @param authorizationCode the authorization code provided by the
*   authorization URL that's used to obtain credentials.
*   @return the credentials that were retrieved from the authorization flow.
*   @throws Exception if retrieving credentials is unsuccessful.
*/
public Credential getAndSaveCredentials(String authorizationCode) throws Exception {
    try {
        GoogleAuthorizationCodeFlow flow = getFlow();
        GoogleClientSecrets googleClientSecrets = getClientSecrets();
        TokenResponse tokenResponse = flow.newTokenRequest(authorizationCode)
            .setClientAuthentication(new ClientParametersAuthentication(
                googleClientSecrets.getWeb().getClientId(),
                googleClientSecrets.getWeb().getClientSecret()))
            .setRedirectUri(REDIRECT_URI)
            .execute();
        Credential credential = flow.createAndStoreCredential(tokenResponse, null);
        return credential;
    } catch (Exception e) {
        throw e;
    }
}

Aggiungi un endpoint per l'URI di reindirizzamento al controller. Recupera il codice di autorizzazione e il parametro state dalla richiesta. Confronta questo parametro state con l'attributo state archiviato nella sessione. Se corrispondono, continua con il flusso di autorizzazione. Se non corrispondono, restituisci un errore.

Quindi, chiama il metodo getAndSaveCredentials AuthService e trasmetti il codice di autorizzazione come parametro. Dopo aver recuperato l'oggetto Credentials, archivialo nella sessione. Poi chiudi la finestra di dialogo e reindirizza l'utente alla pagina di destinazione del componente aggiuntivo.

/** Handles the redirect URL to grant the application access to the user's
*   account.
*   @param request the current request used to obtain the authorization code
*   and state parameter from.
*   @param session the current session.
*   @param response the current response to pass information to.
*   @param model the Model interface to pass error information that's
*   displayed on the error page.
*   @return the close-pop-up template if authorization is successful, or the
*   onError method to handle and display the error message.
*/
@GetMapping(value = {"/callback"})
public String callback(HttpServletRequest request, HttpSession session,
    HttpServletResponse response, Model model) {
    try {
        String authCode = request.getParameter("code");
        String requestState = request.getParameter("state");
        String sessionState = session.getAttribute("state").toString();
        if (!requestState.equals(sessionState)) {
            response.setStatus(401);
            return onError("Invalid state parameter.", model);
        }
        Credential credentials = authService.getAndSaveCredentials(authCode);
        session.setAttribute("credentials", credentials);
        return "close-pop-up";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Testa una chiamata API

Una volta completato il flusso, ora puoi inviare chiamate alle API di Google.

Ad esempio, richiedi le informazioni del profilo dell'utente. Puoi richiedere le informazioni dell'utente dall'API OAuth 2.0.

Python

Leggi la documentazione relativa all'API di rilevamento OAuth 2.0 e utilizzala per inserire un oggetto UserInfo compilato.

# Retrieve the credentials from the session data and construct a
# Credentials instance.
credentials = google.oauth2.credentials.Credentials(
    **flask.session["credentials"])

# Construct the OAuth 2.0 v2 discovery API library.
user_info_service = googleapiclient.discovery.build(
    serviceName="oauth2", version="v2", credentials=credentials)

# Request and store the username in the session.
# This allows it to be used in other methods or in an HTML template.
flask.session["username"] = (
    user_info_service.userinfo().get().execute().get("name"))

Java

Crea un metodo nella classe di servizio che crea un oggetto UserInfo utilizzando Credentials come parametro.

/** Obtains the Userinfo object by passing in the required credentials.
*   @param credentials retrieved from the authorization flow.
*   @return the Userinfo object for the currently signed-in user.
*   @throws IOException if creating UserInfo service or obtaining the
*   Userinfo object is unsuccessful.
*/
public Userinfo getUserInfo(Credential credentials) throws IOException {
    try {
        Oauth2 userInfoService = new Oauth2.Builder(
            new NetHttpTransport(),
            new GsonFactory(),
            credentials).build();
        Userinfo userinfo = userInfoService.userinfo().get().execute();
        return userinfo;
    } catch (Exception e) {
        throw e;
    }
}

Aggiungi l'endpoint /test al controller che mostra l'email dell'utente.

/** Returns the test request page with the user's email.
*   @param session the current session.
*   @param model the Model interface to pass error information that's
*   displayed on the error page.
*   @return the test page that displays the current user's email or the
*   onError method to handle and display the error message.
*/
@GetMapping(value = {"/test"})
public String test(HttpSession session, Model model) {
    try {
        Credential credentials = (Credential) session.getAttribute("credentials");
        Userinfo userInfo = authService.getUserInfo(credentials);
        String userInfoEmail = userInfo.getEmail();
        if (userInfoEmail != null) {
            model.addAttribute("userEmail", userInfoEmail);
        } else {
            return onError("Could not get user email.", model);
        }
        return "test";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Cancella credenziali

Puoi "cancellare" le credenziali di un utente rimuovendole dalla sessione corrente. In questo modo puoi testare il routing sulla pagina di destinazione del componente aggiuntivo.

Ti consigliamo di mostrare un'indicazione che indica che l'utente si è disconnesso prima di reindirizzarlo alla pagina di destinazione del componente aggiuntivo. La tua app dovrebbe seguire il flusso di autorizzazione per ottenere nuove credenziali, ma agli utenti non viene chiesto di autorizzare di nuovo l'app.

Python

@app.route("/clear")
def clear_credentials():
    if "credentials" in flask.session:
        del flask.session["credentials"]
        del flask.session["username"]

    return flask.render_template("signed-out.html")

In alternativa, utilizza flask.session.clear(), ma questa operazione potrebbe avere effetti indesiderati se nella sessione sono archiviati altri valori.

Java

Nel controller, aggiungi un endpoint /clear.

/** Clears the credentials in the session and returns the sign-out
*   confirmation page.
*   @param session the current session.
*   @return the sign-out confirmation page.
*/
@GetMapping(value = {"/clear"})
public String clear(HttpSession session) {
    try {
        if (session != null && session.getAttribute("credentials") != null) {
            session.removeAttribute("credentials");
        }
        return "sign-out";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Revocare l'autorizzazione dell'app

Un utente può revocare l'autorizzazione della tua app inviando una richiesta di POST a https://oauth2.googleapis.com/revoke. La richiesta deve contenere il token di accesso dell'utente.

Python

import requests

@app.route("/revoke")
def revoke():
    if "credentials" not in flask.session:
        return flask.render_template("addon-discovery.html",
                            message="You need to authorize before " +
                            "attempting to revoke credentials.")

    credentials = google.oauth2.credentials.Credentials(
        **flask.session["credentials"])

    revoke = requests.post(
        "https://oauth2.googleapis.com/revoke",
        params={"token": credentials.token},
        headers={"content-type": "application/x-www-form-urlencoded"})

    if "credentials" in flask.session:
        del flask.session["credentials"]
        del flask.session["username"]

    status_code = getattr(revoke, "status_code")
    if status_code == 200:
        return flask.render_template("authorization.html")
    else:
        return flask.render_template(
            "index.html", message="An error occurred during revocation!")

Java

Aggiungi un metodo alla classe di servizio che effettua una chiamata all'endpoint di revoca.

/** Revokes the app's permissions to the user's account.
*   @param credentials retrieved from the authorization flow.
*   @return response entity returned from the HTTP call to obtain response
*   information.
*   @throws RestClientException if the POST request to the revoke endpoint is
*   unsuccessful.
*/
public ResponseEntity<String> revokeCredentials(Credential credentials) throws RestClientException {
    try {
        String accessToken = credentials.getAccessToken();
        String url = "https://oauth2.googleapis.com/revoke?token=" + accessToken;

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
        HttpEntity<Object> httpEntity = new HttpEntity<Object>(httpHeaders);
        ResponseEntity<String> responseEntity = new RestTemplate().exchange(
            url,
            HttpMethod.POST,
            httpEntity,
            String.class);
        return responseEntity;
    } catch (RestClientException e) {
        throw e;
    }
}

Aggiungi un endpoint, /revoke, al controller che cancelli la sessione e reindirizzi l'utente alla pagina di autorizzazione se la revoca è riuscita.

/** Revokes the app's permissions and returns the authorization page.
*   @param session the current session.
*   @return the authorization page.
*   @throws Exception if revoking access is unsuccessful.
*/
@GetMapping(value = {"/revoke"})
public String revoke(HttpSession session) throws Exception {
    try {
        if (session != null && session.getAttribute("credentials") != null) {
            Credential credentials = (Credential) session.getAttribute("credentials");
            ResponseEntity responseEntity = authService.revokeCredentials(credentials);
            Integer httpStatusCode = responseEntity.getStatusCodeValue();

            if (httpStatusCode != 200) {
                return onError("There was an issue revoking access: " +
                    responseEntity.getStatusCode(), model);
            }
            session.removeAttribute("credentials");
        }
        return startAuthFlow(model);
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Testa il componente aggiuntivo

Accedi a Google Classroom come utente di test Insegnante. Vai alla scheda Lavori del corso e crea un nuovo Compito. Fai clic sul pulsante Componenti aggiuntivi sotto l'area di testo, quindi seleziona il componente aggiuntivo. L'iframe si apre e il componente aggiuntivo carica l'URI di configurazione dell'allegato che hai specificato nella pagina Configurazione app dell'SDK GWM.

Complimenti! Puoi passare al passaggio successivo: gestire le visite ripetute al componente aggiuntivo.