Connecter l'utilisateur

Il s'agit du deuxième tutoriel de la série sur les modules complémentaires Classroom.

Dans ce tutoriel, vous allez ajouter Google Sign-in à l'application Web. Ce comportement est obligatoire pour les modules complémentaires Classroom. Utilisez les identifiants de ce flux d'autorisation pour tous les futurs appels de l'API.

Dans ce tutoriel, vous allez:

  • Configurez votre application Web pour gérer les données de session dans un iFrame.
  • Implémenter le flux de connexion de serveur à serveur de Google OAuth 2.0
  • Envoyez un appel à l'API OAuth 2.0.
  • Créez des routes supplémentaires pour permettre l'autorisation, la déconnexion et le test des appels d'API.

Une fois que vous avez terminé, vous pouvez autoriser entièrement les utilisateurs dans votre application Web et émettre des appels vers les API Google.

Comprendre le flux d'autorisation

Les API Google utilisent le protocole OAuth 2.0 pour l'authentification et l'autorisation. La description complète de la mise en œuvre OAuth de Google est disponible dans le guide OAuth de Google Identity.

Les identifiants de votre application sont gérés dans Google Cloud. Une fois ces paramètres créés, implémentez un processus en quatre étapes pour authentifier et autoriser un utilisateur:

  1. Demandez l'autorisation. Fournissez une URL de rappel dans cette requête. Une fois l'opération terminée, vous recevez une URL d'autorisation.
  2. Redirigez l'utilisateur vers l'URL d'autorisation. La page qui s'affiche informe l'utilisateur des autorisations requises par votre application et l'invite à autoriser l'accès. Une fois l'opération terminée, l'utilisateur est redirigé vers l'URL de rappel.
  3. Recevez un code d'autorisation sur votre itinéraire de rappel. Échangez le code d'autorisation contre un jeton d'accès et un jeton d'actualisation.
  4. Appelez une API Google à l'aide des jetons.

Obtenir des identifiants OAuth 2.0

Assurez-vous d'avoir créé et téléchargé des identifiants OAuth comme décrit sur la page "Présentation". Votre projet doit utiliser ces identifiants pour connecter l'utilisateur.

Implémenter le flux d'autorisation

Ajoutez une logique et des routes à notre application Web pour réaliser le flux décrit, y compris les fonctionnalités suivantes:

  • Lancez la procédure d'autorisation lorsque vous atteignez la page de destination.
  • Demandez l'autorisation et gérez la réponse du serveur d'autorisation.
  • Effacez les identifiants stockés.
  • Révoquez les autorisations de l'application.
  • Tester un appel d'API

Lancer l'autorisation

Si nécessaire, modifiez votre page de destination pour lancer le flux d'autorisation. Le module complémentaire peut avoir deux états possibles : des jetons sont enregistrés dans la session en cours ou vous devez les obtenir auprès du serveur OAuth 2.0. Effectuez un appel d'API test s'il existe des jetons dans la session ou invitez l'utilisateur à se connecter.

Python

Ouvrez votre fichier routes.py. Commencez par définir quelques constantes et la configuration de nos cookies conformément aux recommandations de sécurité pour les cadres 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",
)

Accédez à l'itinéraire d'atterrissage du module complémentaire (/classroom-addon dans l'exemple de fichier). Ajoutez une logique pour afficher une page de connexion si la session ne contient pas la clé "identifiants".

@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

Le code de ce tutoriel est disponible dans le module step_02_sign_in.

Ouvrez le fichier application.properties, puis ajoutez une configuration de session qui suit les recommandations de sécurité pour les cadres 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

Créez une classe de service (AuthService.java dans le module step_02_sign_in) pour gérer la logique derrière les points de terminaison dans le fichier du contrôleur, puis configurez l'URI de redirection, l'emplacement du fichier de secrets client et les niveaux d'accès requis par votre module complémentaire. L'URI de redirection permet de rediriger les utilisateurs vers un URI spécifique une fois qu'ils ont autorisé votre application. Consultez la section de configuration du projet du fichier README.md dans le code source pour savoir où placer votre fichier 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));
    }
}

Ouvrez le fichier de contrôleur (AuthController.java dans le module step_02_sign_in) et ajoutez une logique à la route de destination pour afficher la page de connexion si la session ne contient pas la clé 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);
    }
}

Votre page d'autorisation doit contenir un lien ou un bouton permettant à l'utilisateur de "se connecter". En cliquant dessus, l'utilisateur est redirigé vers l'itinéraire authorize.

Autorisation de requête

Pour demander une autorisation, créez et redirigez l'utilisateur vers une URL d'authentification. Cette URL inclut plusieurs informations, telles que les champs d'application demandés, la route de destination pour l'autorisation après l'autorisation et l'ID client de l'application Web. Vous les trouverez dans cet exemple d'URL d'autorisation.

Python

Ajoutez l'importation suivante à votre fichier routes.py.

import google_auth_oauthlib.flow

Créez une route /authorize. Créez une instance de google_auth_oauthlib.flow.Flow. Pour ce faire, nous vous recommandons vivement d'utiliser la méthode from_client_secrets_file incluse.

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

Définissez le redirect_uri de flow. Il s'agit de la route sur laquelle vous souhaitez que les utilisateurs reviennent après avoir autorisé votre application (/callback dans l'exemple suivant).

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

Utilisez l'objet Flow pour construire les éléments authorization_url et state. Stockez le state dans la session. Il permettra de vérifier ultérieurement l'authenticité de la réponse du serveur. Enfin, redirigez l'utilisateur vers 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

Ajoutez les méthodes suivantes au fichier AuthService.java pour instancier l'objet de flux, puis utilisez-le pour récupérer l'URL d'autorisation:

  • La méthode getClientSecrets() lit le fichier secret du client et construit un objet GoogleClientSecrets.
  • La méthode getFlow() crée une instance de GoogleAuthorizationCodeFlow.
  • La méthode authorize() utilise l'objet GoogleAuthorizationCodeFlow, le paramètre state et l'URI de redirection pour récupérer l'URL d'autorisation. Le paramètre state permet de vérifier l'authenticité de la réponse du serveur d'autorisation. La méthode renvoie ensuite un mappage avec l'URL d'autorisation et le paramètre 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;
    }
}

Utilisez l'injection par constructeur pour créer une instance de la classe de service dans la classe du contrôleur.

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

Ajoutez le point de terminaison /authorize à la classe du contrôleur. Ce point de terminaison appelle la méthode AuthService authorize() pour récupérer le paramètre state et l'URL d'autorisation. Ensuite, le point de terminaison stocke le paramètre state dans la session et redirige les utilisateurs vers l'URL d'autorisation.

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

Gérer la réponse du serveur

Une fois l'autorisation accordée, l'utilisateur revient à la route redirect_uri de l'étape précédente. Dans l'exemple précédent, cet itinéraire est /callback.

Vous recevez un code dans la réponse lorsque l'utilisateur quitte la page d'autorisation. Échangez ensuite le code contre des jetons d'accès et d'actualisation:

Python

Ajoutez les importations suivantes à votre fichier serveur Flask.

import google.oauth2.credentials
import googleapiclient.discovery

Ajoutez la route à votre serveur. Créez une autre instance de google_auth_oauthlib.flow.Flow, mais cette fois, réutilisez l'état enregistré à l'étape précédente.

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

Demandez ensuite des jetons d'accès et d'actualisation. Heureusement, l'objet flow contient également la méthode fetch_token pour y parvenir. La méthode attend les arguments code ou authorization_response. Utilisez authorization_response, car il s'agit de l'URL complète de la requête.

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

Vous avez désormais des identifiants complets ! Stockez-les dans la session afin qu'ils puissent être récupérés par d'autres méthodes ou itinéraires, puis redirigez-les vers une page de destination de module complémentaire.

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

Ajoutez à votre classe de service une méthode qui renvoie l'objet Credentials en transmettant le code d'autorisation récupéré à partir de la redirection effectuée par l'URL d'autorisation. Cet objet Credentials sera utilisé ultérieurement pour récupérer le jeton d'accès et le jeton d'actualisation.

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

Ajoutez un point de terminaison pour votre URI de redirection au contrôleur. Récupérez le code d'autorisation et le paramètre state de la requête. Comparez ce paramètre state à l'attribut state stocké dans la session. S'ils correspondent, poursuivez le flux d'autorisation. Si ce n'est pas le cas, une erreur est renvoyée.

Appelez ensuite la méthode getAndSaveCredentials AuthService et transmettez le code d'autorisation en tant que paramètre. Après avoir récupéré l'objet Credentials, stockez-le dans la session. Fermez ensuite la boîte de dialogue et redirigez l'utilisateur vers la page de destination du module complémentaire.

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

Tester un appel d'API

Une fois le flux terminé, vous pouvez émettre des appels vers les API Google.

Par exemple, demandez les informations de profil de l'utilisateur. Vous pouvez demander les informations de l'utilisateur à partir de l'API OAuth 2.0.

Python

Lisez la documentation de l'API de découverte OAuth 2.0. Utilisez-la pour obtenir un objet UserInfo renseigné.

# 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

Créez une méthode dans la classe de service qui compile un objet UserInfo en utilisant Credentials comme paramètre.

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

Ajoutez le point de terminaison /test au contrôleur qui affiche l'adresse e-mail de l'utilisateur.

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

Effacer les identifiants

Vous pouvez "effacer" les identifiants d'un utilisateur en le supprimant de la session en cours. Cela vous permet de tester l'itinéraire sur la page de destination du module complémentaire.

Nous vous recommandons d'indiquer que l'utilisateur s'est déconnecté avant de le rediriger vers la page de destination du module complémentaire. Votre application doit passer par le flux d'autorisation pour obtenir de nouveaux identifiants, mais les utilisateurs ne sont pas invités à autoriser à nouveau votre application.

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

Vous pouvez également utiliser flask.session.clear(), mais cela peut avoir des effets inattendus si d'autres valeurs sont stockées dans la session.

Java

Dans le contrôleur, ajoutez un point de terminaison /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);
    }
}

Révoquer l'autorisation de l'application

Un utilisateur peut révoquer l'autorisation de votre application en envoyant une requête POST à https://oauth2.googleapis.com/revoke. La requête doit contenir le jeton d'accès de l'utilisateur.

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

Ajoutez à la classe de service une méthode qui appelle le point de terminaison révoqué.

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

Ajoutez un point de terminaison /revoke au contrôleur qui efface la session et redirige l'utilisateur vers la page d'autorisation si la révocation a abouti.

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

Tester le module complémentaire

Connectez-vous à Google Classroom en tant qu'utilisateur test Enseignant. Accédez à l'onglet Travaux et devoirs et créez un devoir. Cliquez sur le bouton Modules complémentaires sous la zone de texte, puis sélectionnez votre module complémentaire. L'iFrame s'ouvre et le module complémentaire charge l'URI de configuration des pièces jointes que vous avez spécifié sur la page Configuration de l'application du SDK GWM.

Félicitations ! Vous êtes prêt à passer à l'étape suivante: gérer les visites répétées de votre module complémentaire.