Consenti l'accesso all'utente

Questa è la seconda procedura dettagliata della serie dettagliata dei componenti aggiuntivi di Classroom.

In questa procedura dettagliata aggiungerai Accedi con Google all'applicazione web. Si tratta di 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, completerai quanto segue:

  • Configura l'app web in modo da conservare i dati della sessione all'interno di un iframe.
  • Implementa il flusso di accesso server-server per 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 e inviare 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 di OAuth di Google è disponibile nella guida OAuth di Google Identity.

Le credenziali della tua applicazione sono gestite in Google Cloud. Una volta creati, implementa una procedura di quattro passaggi per autenticare e autorizzare un utente:

  1. Richiedi l'autorizzazione. Fornisci un URL di callback come parte della 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 al tuo 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 progetto deve utilizzare queste credenziali per eseguire l'accesso dell'utente.

Implementare il flusso di autorizzazione

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

  • Avvia il flusso di autorizzazione una volta raggiunta la pagina di destinazione.
  • Richiedi l'autorizzazione e gestisci la risposta del server di autorizzazione.
  • Cancella le credenziali memorizzate.
  • Revocare le autorizzazioni dell'app.
  • Testa 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 stati: sono presenti token salvati nella sessione corrente o devi ottenere token dal server OAuth 2.0. Esegui una chiamata API di prova se sono presenti token nella sessione oppure chiedi 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 consigli 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",
)

Spostati sul percorso di destinazione del componente aggiuntivo (/classroom-addon nel file di esempio). Aggiungi logica per eseguire il rendering di 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 è disponibile nel modulo step_02_sign_in.

Apri il file application.properties e aggiungi una configurazione della sessione che segui i consigli per la 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 imposta 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 sulla 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 del controller (AuthController.java nel modulo step_02_sign_in) e aggiungi logica alla route di destinazione per eseguire il rendering della 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 fai clic su questo pulsante, l'utente dovrebbe essere reindirizzato al percorso authorize.

Richiesta 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 verificarli in questo esempio di URL di autorizzazione.

Python

Aggiungi la seguente importazione al tuo file routes.py.

import google_auth_oauthlib.flow

Crea un nuovo percorso (/authorize). Crea un'istanza di google_auth_oauthlib.flow.Flow; ti consigliamo vivamente di utilizzare il metodo from_client_secrets_file incluso per farlo.

@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 del percorso verso cui vuoi che gli utenti tornino dopo aver autorizzato la tua app. Si tratta di /callback nell'esempio che segue.

# 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 l'autenticità della risposta del server in un secondo momento. 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 flow, quindi utilizzalo per recuperare l'URL di autorizzazione:

  • Il metodo getClientSecrets() legge il file del client secret e genera 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;
    }
}

Usa 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 controller. Questo endpoint chiama il metodo authorize() di AuthService per recuperare il parametro state e l'URL di autorizzazione. Quindi, l'endpoint archivia 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, questa route è /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 eseguire questa operazione. 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, quindi reindirizzandoli alla 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

Aggiungere alla classe di servizio un metodo che restituisca l'oggetto Credentials passando 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 il token di aggiornamento.

/** 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 al controller un endpoint per l'URI di reindirizzamento. 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.

Poi, chiama il metodo getAndSaveCredentials AuthService e inserisci il codice di autorizzazione come parametro. Dopo aver recuperato l'oggetto Credentials, archivialo nella sessione. Quindi, 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

Completata la procedura, puoi effettuare chiamate alle API di Google.

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

Python

Leggi la documentazione dell'API di rilevamento OAuth 2.0 Utilizzala per ottenere 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 per creare 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 l'utente ha eseguito la disconnessione prima di reindirizzarlo alla pagina di destinazione del componente aggiuntivo. L'app dovrebbe essere sottoposta al flusso di autorizzazione per ottenere le nuove credenziali, ma agli utenti non viene chiesto di autorizzare nuovamente 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 hai altri valori archiviati nella sessione.

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

Revoca l'autorizzazione dell'app

Un utente può revocare l'autorizzazione della tua app inviando una richiesta 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 alla classe di servizio un metodo che effettui 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 al controller un endpoint, /revoke, che cancella la sessione e reindirizzi l'utente alla pagina di autorizzazione se la revoca è andata a buon fine.

/** 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 per insegnanti. Vai alla scheda Lavori del corso e crea un nuovo compito. Fai clic sul pulsante Componenti aggiuntivi sotto l'area di testo e seleziona il componente aggiuntivo. L'iframe si apre e il componente aggiuntivo carica l'URI di configurazione degli allegati specificato nella pagina Configurazione app dell'SDK GWM.

Complimenti! Sei pronto per procedere con il passaggio successivo: gestione di visite ripetute al componente aggiuntivo.