Connecter l'utilisateur

Il s'agit du deuxième tutoriel concernant les modules complémentaires Classroom cette série de tutoriels.

Dans ce tutoriel, vous allez ajouter Google Sign-in à l'application Web. Il s'agit d'un comportement requis pour les modules complémentaires Classroom. Utiliser les identifiants fournis par 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éer des routes supplémentaires pour permettre l'autorisation, la déconnexion et les tests Appels d'API.

Une fois l'opération terminée, vous pouvez autoriser complètement les utilisateurs dans votre application Web et émettre des appels vers aux 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 la Guide OAuth Google Identity

Les identifiants de votre application sont gérés dans Google Cloud. Une fois qu'ils ont sont créées, mettez en œuvre un processus en quatre étapes pour authentifier 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 en résulte informe utilisateur des autorisations requises par votre application et l'invite à accorder 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 un code d'autorisation pour 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 dans la section la page Vue d'ensemble. 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 ces fonctionnalités:

  • 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. La peut avoir deux états possibles : soit des jetons sont enregistrés session en cours, ou vous devez obtenir des jetons auprès du serveur OAuth 2.0. Exécuter un appel d'API test s'il existe des jetons dans la session, ou demandez à l'utilisateur pour vous connecter.

Python

Ouvrez votre fichier routes.py. Commencez par définir quelques constantes et notre cookie conformément aux recommandations de sécurité concernant 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 de destination du module complémentaire (/classroom-addon dans l'exemple). ). Ajouter une logique pour afficher une page de connexion si la session ne contient pas les "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 et ajoutez une configuration de session qui respecte les recommandations de sécurité concernant 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éer 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 et configurer l'URI de redirection, l'emplacement du fichier de secrets client et les champs d'application que votre module complémentaire requiert. L'URI de redirection permet de rediriger les utilisateurs vers un URI spécifique. après avoir autorisé votre application. Consultez la section Configuration du projet du README.md dans le code source pour savoir où placer les 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 la manette (AuthController.java dans le fichier step_02_sign_in). ) et d'ajouter une logique à l'itinéraire de destination pour afficher la page de connexion si le 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 "signer ". 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 un URL. Cette URL inclut plusieurs informations, telles que les champs d'application demandée, la route de destination pour l'autorisation après l'autorisation et le paramètre ID client. 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éer une instance de google_auth_oauthlib.flow.Flow; nous vous recommandons vivement d'utiliser from_client_secrets_file.

@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 vers laquelle vous souhaitez que les utilisateurs à renvoyer après avoir autorisé votre application. Il s'agit de /callback dans à titre d'exemple.

# 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. Magasin le state de la session ; permet de vérifier l'authenticité 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 flow, puis utilisez-le pour récupérer l'URL d'autorisation:

  • La méthode getClientSecrets() lit le fichier secret du client et crée un objet GoogleClientSecrets.
  • La méthode getFlow() crée une instance de GoogleAuthorizationCodeFlow.
  • La méthode authorize() utilise l'objet GoogleAuthorizationCodeFlow, 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. à partir du serveur d'autorisation. La méthode renvoie ensuite une carte avec la 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 le de la classe 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 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

Après l'autorisation, l'utilisateur revient à l'itinéraire redirect_uri à partir 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 revient depuis le 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. Construisez une autre instance de google_auth_oauthlib.flow.Flow, mais réutilisez cette fois l'état enregistré dans à 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 la méthode fetch_token pour y parvenir. La méthode attend les arguments code ou authorization_response. Utilisez les 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 peuvent être récupérées via d'autres méthodes ou routes, puis sont redirigés vers un module complémentaire page de destination.

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 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érer les et le paramètre state de la requête. Comparer le paramètre state à l'attribut state stocké dans la session. Si cette personne avant de poursuivre le flux d'autorisation. Si ce n'est pas le cas, renvoie une erreur.

Appelez ensuite la méthode getAndSaveCredentials AuthService et transmettez la valeur le code d'autorisation en tant que paramètre. Après avoir récupéré la Credentials , stockez-le dans la session. Fermez ensuite la boîte de dialogue et redirigez le 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 le les informations de l'utilisateur à partir de l'API OAuth 2.0.

Python

Lisez la documentation API de découverte OAuth 2.0 Utilisez-le 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

Dans la classe de service, créez une méthode qui compile un objet UserInfo à l'aide de 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éjà déconnecté auparavant les rediriger vers la page de destination du module complémentaire. Votre application doit passer par 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 cette action peut avoir des effets indésirables 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 les informations un jeton d'accès.

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 l'utilisateur est redirigé vers la page d'autorisation si la révocation a été réussi.

/** 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, puis créer 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 URI de configuration de la pièce jointe que vous avez spécifié dans l'application du SDK GWM Configuration .

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