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. Il s'agit d'un comportement obligatoire pour les modules complémentaires Classroom. Utilisez les identifiants de ce flux d'autorisation pour tous les futurs appels à l'API.
Au cours de ce tutoriel, vous allez effectuer les opérations suivantes:
- Configurez votre application Web de façon à gérer les données de session dans un iFrame.
- Implémentez le flux de connexion de serveur à serveur Google OAuth 2.0.
- Émettez un appel à l'API OAuth 2.0.
- Créez des routes supplémentaires pour prendre en charge l'autorisation, la déconnexion et le test des appels d'API.
Une fois terminé, vous pouvez autoriser complètement 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 l'implémentation 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 ceux-ci créés, implémentez un processus en quatre étapes pour authentifier et autoriser un utilisateur:
- 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.
- 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.
- Recevoir un code d'autorisation sur votre route de rappel. Échangez le code d'autorisation contre un jeton d'accès et un jeton d'actualisation.
- 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 de la logique et des routes à notre application Web pour réaliser le flux décrit, y compris les fonctionnalités suivantes:
- Démarrez le flux d'autorisation lorsque vous accédez à 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 se trouver dans deux états : soit des jetons sont enregistrés dans la session en cours, soit vous devez obtenir des jetons auprès du serveur OAuth 2.0. Effectuez un appel d'API de 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 notre configuration de cookie conformément aux recommandations de sécurité pour les iFrames.
# 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 à la page de destination de votre 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, et configurez l'URI de redirection, l'emplacement du fichier de secrets client et les portées requises par votre module complémentaire. L'URI de redirection permet de rediriger vos utilisateurs vers un URI spécifique après qu'ils ont autorisé votre application. Consultez la section "Project Setup" (Configuration du projet) de 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 du contrôleur (AuthController.java
dans le module step_02_sign_in
) et ajoutez une logique au parcours 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 doit être 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, l'itinéraire de destination pour après l'autorisation et l'ID client de l'application Web. Vous pouvez les voir 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
. Nous vous recommandons vivement d'utiliser la méthode from_client_secrets_file
incluse pour ce faire.
@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 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 de flux pour créer 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 crée un objetGoogleClientSecrets
. - La méthode
getFlow()
crée une instance deGoogleAuthorizationCodeFlow
. - La méthode
authorize()
utilise l'objetGoogleAuthorizationCodeFlow
, le paramètrestate
et l'URI de redirection pour récupérer l'URL d'autorisation. Le paramètrestate
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ètrestate
.
/** 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 de constructeur pour créer une instance de la classe de service dans la classe de 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 authorize()
d'AuthService pour récupérer le paramètre state
et l'URL d'autorisation. Le point de terminaison stocke ensuite 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
Après l'autorisation, l'utilisateur revient au parcours 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 revient de la page d'autorisation. Échangez ensuite le code contre des jetons d'accès et d'actualisation:
Python
Ajoutez les importations suivantes à votre fichier de serveur Flask.
import google.oauth2.credentials
import googleapiclient.discovery
Ajoutez le routage à 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 disposez désormais de toutes les informations requises. Stockez-les dans la session afin qu'ils puissent être récupérés dans d'autres méthodes ou routes, puis redirigez 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
à partir de la requête. Comparez ce paramètre state
à l'attribut state
stocké dans la session. Si c'est le cas, poursuivez la procédure d'autorisation. Si ce n'est pas le cas, renvoyez une erreur.
Appelez ensuite la méthode getAndSaveCredentials
de 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
Consultez la documentation de l'API Discovery 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 crée 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 les 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 suivre le flux d'autorisation pour obtenir de nouveaux identifiants, mais les utilisateurs ne sont pas invités à réautoriser 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 involontaires 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 une méthode à la classe de service qui appelle le point de terminaison de révocation.
/** 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 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'un de vos utilisateurs 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 de l'attachement 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 sur votre module complémentaire.