Esta es la segunda explicación de la serie de instructivos sobre complementos de Classroom.
En esta explicación, agregarás el Acceso con Google a la aplicación web. Este es un comportamiento obligatorio para los complementos de Classroom. Usa las credenciales de este flujo de autorización para todas las llamadas futuras a la API.
En esta explicación, completaste lo siguiente:
- Configura tu aplicación web para que mantenga los datos de las sesiones en un iframe.
- Implementa el flujo de acceso de servidor a servidor de OAuth 2.0 de Google.
- Realiza una llamada a la API de OAuth 2.0.
- Crea rutas adicionales para admitir la autorización, el cierre de sesión y la prueba de llamadas a la API.
Una vez que termines, podrás autorizar por completo a los usuarios en tu app web y realizar llamadas a las APIs de Google.
Comprende el flujo de autorización
Las APIs de Google usan el protocolo OAuth 2.0 para la autenticación y la autorización. La descripción completa de la implementación de OAuth de Google está disponible en la Guía de OAuth de Google Identity.
Las credenciales de tu aplicación se administran en Google Cloud. Una vez que se hayan creado, implementa un proceso de cuatro pasos para autenticar y autorizar a un usuario:
- Solicitar autorización Proporciona una URL de devolución de llamada como parte de esta solicitud. Cuando se complete, recibirás una URL de autorización.
- Redirecciona al usuario a la URL de autorización. La página resultante le informa al usuario los permisos que requiere tu app y le solicita que permita el acceso. Cuando se complete, se redireccionará al usuario a la URL de devolución de llamada.
- Recibir un código de autorización en tu ruta de devolución de llamada Intercambia el código de autorización por un token de acceso y un token de actualización.
- Realiza llamadas a una API de Google con los tokens.
Obtén credenciales de OAuth 2.0
Asegúrate de haber creado y descargado credenciales de OAuth como se describe en la página Descripción general. Tu proyecto debe usar estas credenciales para permitir que el usuario acceda.
Implementa el flujo de autorización
Agrega lógica y rutas a nuestra app web para realizar el flujo descrito, incluidas estas funciones:
- Inicia el flujo de autorización cuando llegues a la página de destino.
- Solicita autorización y controla la respuesta del servidor de autorización.
- Borra las credenciales almacenadas.
- Revoca los permisos de la app.
- Probar una llamada a la API
Inicia la autorización
Si es necesario, modifica la página de destino para iniciar el flujo de autorización. El complemento puede tener dos estados posibles: hay tokens guardados en la sesión actual o necesitas obtener tokens del servidor de OAuth 2.0. Realiza una llamada a la API de prueba si hay tokens en la sesión o, de lo contrario, solicita al usuario que inicie sesión.
Python
Abre el archivo routes.py
. Primero, establece algunas constantes y nuestra configuración de cookies según las recomendaciones de seguridad de 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",
)
Ve a la ruta de destino del complemento (es /classroom-addon
en el archivo de ejemplo). Agrega lógica para renderizar una página de acceso si la sesión no contiene la clave "credentials".
@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
El código de esta explicación se encuentra en el módulo step_02_sign_in
.
Abre el archivo application.properties
y agrega la configuración de la sesión que siga las recomendaciones de seguridad de 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 clase de servicio (AuthService.java
en el módulo step_02_sign_in
)
para controlar la lógica detrás de los extremos en el archivo del controlador y configurar
el URI de redireccionamiento, la ubicación del archivo de secretos de cliente y los permisos que requiere tu complemento. El URI de redireccionamiento se usa para redirigir a los usuarios a un URI específico después de que autorizan la app. Consulta la sección Configuración del proyecto de README.md
en el código fuente para obtener información sobre dónde colocar el archivo 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));
}
}
Abre el archivo del controlador (AuthController.java
en el módulo step_02_sign_in
) y agrega lógica a la ruta de destino para renderizar la página de acceso si la sesión no contiene la clave 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);
}
}
Tu página de autorización debe contener un vínculo o un botón para que el usuario "acceda". Al hacer clic en este botón, se debería redireccionar al usuario a la ruta authorize
.
Solicitar autorización
Para solicitar autorización, crea una URL de autenticación y redirecciona al usuario a ella. Esta URL incluye varios datos, como los permisos solicitados, la ruta de destino para después de la autorización y el ID de cliente de la app web. Puedes ver estos campos en esta URL de autorización de ejemplo.
Python
Agrega la siguiente importación a tu archivo routes.py
.
import google_auth_oauthlib.flow
Crea una nueva ruta /authorize
. Crea una instancia de google_auth_oauthlib.flow.Flow
; te recomendamos que uses el método from_client_secrets_file
incluido para hacerlo.
@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)
Establece el redirect_uri
de flow
, que es la ruta a la que deseas que los usuarios regresen después de autorizar tu app. Esta es /callback
en el siguiente ejemplo.
# 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)
Usa el objeto de flujo para construir authorization_url
y state
. Almacena el state
en la sesión; se usa para verificar la autenticidad de la respuesta del servidor más adelante. Por último, redirecciona al usuario a la 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
Agrega los siguientes métodos al archivo AuthService.java
para crear una instancia del objeto de flujo y, luego, úsalo para recuperar la URL de autorización:
- El método
getClientSecrets()
lee el archivo de secretos del cliente y construye un objetoGoogleClientSecrets
. - El método
getFlow()
crea una instancia deGoogleAuthorizationCodeFlow
. - El método
authorize()
usa el objetoGoogleAuthorizationCodeFlow
, el parámetrostate
y el URI de redireccionamiento para recuperar la URL de autorización. El parámetrostate
se usa para verificar la autenticidad de la respuesta del servidor de autorización. Luego, el método muestra un mapa con la URL de autorización y el parámetrostate
.
/** 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 la inserción de constructor para crear una instancia de la clase de servicio en la clase del controlador.
/** 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;
}
Agrega el extremo /authorize
a la clase del controlador. Este extremo llama al método authorize()
de AuthService para recuperar el parámetro state
y la URL de autorización. Luego, el extremo almacena el parámetro state
en la sesión y redirecciona a los usuarios a la URL de autorización.
/** 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;
}
}
Controla la respuesta del servidor
Después de la autorización, el usuario vuelve a la ruta redirect_uri
del paso anterior. En el ejemplo anterior, esta ruta es /callback
.
Recibirás una code
en la respuesta cuando el usuario regrese de la página de autorización. Luego, intercambia el código por tokens de acceso y actualización:
Python
Agrega las siguientes importaciones a tu archivo del servidor de Flask.
import google.oauth2.credentials
import googleapiclient.discovery
Agrega la ruta a tu servidor. Crea otra instancia de google_auth_oauthlib.flow.Flow
, pero esta vez reutiliza el estado guardado en el paso anterior.
@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)
A continuación, solicita tokens de acceso y actualización. Por suerte, el objeto flow
también contiene el método fetch_token
para lograr esto. El método espera los argumentos code
o authorization_response
. Usa authorization_response
, ya que es la URL completa de la solicitud.
authorization_response = flask.request.url
flow.fetch_token(authorization_response=authorization_response)
Ahora tienes las credenciales completas. Almacenarlos en la sesión para que se puedan recuperar en otros métodos o rutas y, luego, redireccionar a una página de destino del complemento
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
Agrega un método a tu clase de servicio que devuelva el objeto Credentials
pasando el código de autorización recuperado del redireccionamiento que realiza la URL de autorización. Este objeto Credentials
se usará más adelante para recuperar el token de acceso y el de actualización.
/** 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;
}
}
Agrega un extremo para tu URI de redireccionamiento al controlador. Recupera el código de autorización y el parámetro state
de la solicitud. Compara este parámetro state
con el atributo state
almacenado en la sesión. Si coinciden, continúa con el flujo de autorización. Si no coinciden, se muestra un error.
Luego, llama al método getAndSaveCredentials
de AuthService
y pasa el código de autorización como parámetro. Después de recuperar el objeto Credentials
, almacénalo en la sesión. Luego, cierra el diálogo y redirecciona al usuario a la página de destino del complemento.
/** 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);
}
}
Prueba una llamada a la API
Ahora que se completó el flujo, puedes realizar llamadas a las APIs de Google.
Por ejemplo, solicita la información del perfil del usuario. Puedes solicitar la información del usuario desde la API de OAuth 2.0.
Python
Lee la documentación de la API de descubrimiento de OAuth 2.0. Úsala para obtener un objeto UserInfo propagado.
# 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 método en la clase de servicio que compile un objeto UserInfo
con Credentials
como parámetro.
/** 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;
}
}
Agrega el extremo /test
al controlador que muestra el correo electrónico del usuario.
/** 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);
}
}
Borrar credenciales
Para "borrar" las credenciales de un usuario, quítalas de la sesión actual. Esto te permite probar el enrutamiento en la página de destino del complemento.
Te recomendamos que muestres una indicación de que el usuario salió de su cuenta antes de redireccionarlo a la página de destino del complemento. Tu app debe pasar por el flujo de autorización para obtener credenciales nuevas, pero no se les solicita a los usuarios que vuelvan a autorizar tu 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")
Como alternativa, usa flask.session.clear()
, pero esto puede tener efectos no deseados si tienes otros valores almacenados en la sesión.
Java
En el controlador, agrega un extremo /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);
}
}
Cómo revocar el permiso de la app
Un usuario puede revocar el permiso de tu app enviando una solicitud POST
a https://oauth2.googleapis.com/revoke
. La solicitud debe contener el token de acceso del usuario.
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
Agrega un método a la clase de servicio que realice una llamada al extremo de revocación.
/** 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;
}
}
Agrega un extremo, /revoke
, al controlador que borre la sesión y
redireccione al usuario a la página de autorización si la revocación se realizó
correctamente.
/** 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);
}
}
Prueba el complemento
Accede a Google Classroom como uno de tus usuarios de prueba Profesor. Navega a la pestaña Trabajo en clase y crea una nueva Tarea. Haz clic en el botón Complementos debajo del área de texto y, luego, selecciona el complemento. Se abre el iframe y el complemento carga el URI de configuración de adjuntos que especificaste en la página Configuración de la app del SDK de GWM.
¡Felicitaciones! Ya puedes continuar con el siguiente paso: controlar las visitas repetidas a tu complemento.