Permite que el usuario acceda

Esta es la segunda explicación en los complementos de Classroom .

En esta explicación, agregarás el Acceso con Google a la aplicación web. Este es un el comportamiento requerido de 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.
  • Implementar el flujo de acceso de servidor a servidor de Google OAuth 2.0
  • Realiza una llamada a la API de OAuth 2.0.
  • Crea rutas adicionales para admitir la autorización, el cierre de sesión y las pruebas Llamadas a la API.

Una vez finalizado, puedes autorizar completamente a los usuarios en tu aplicación web y emitir llamadas a 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. Puedes encontrar la descripción completa de la implementación de OAuth de Google en el Guía de OAuth de Google Identity.

Las credenciales de tu aplicación se administran en Google Cloud. Una vez que tengan un proceso de cuatro pasos para autenticar y autorizar usuario:

  1. Autorización de solicitud Proporciona una URL de devolución de llamada como parte de esta solicitud. Cuando se complete, recibirás una URL de autorización.
  2. Redireccionar al usuario a la URL de autorización La página resultante informa al usuario de los permisos que requiere tu app y le solicita que permita el acceso. Cuando se completa, se dirige al usuario a la URL de devolución de llamada.
  3. Recibir un código de autorización en tu ruta de devolución de llamada Intercambia el código de autorización de un token de acceso y un token de actualización.
  4. Realiza llamadas a una API de Google usando los tokens.

Obtén credenciales de OAuth 2.0

Asegúrate de haber creado y descargado las credenciales de OAuth como se describe en la página Resumen. Tu proyecto debe usar estas credenciales para que el usuario acceda.

Cómo implementar el flujo de autorización

Agregar lógica y rutas a nuestra aplicación web para obtener el flujo descrito, lo que incluye 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.
  • Revocar los permisos de la app
  • Probar una llamada a la API

Iniciar autorización

Si es necesario, modifica la página de destino para iniciar el flujo de autorización. El el complemento puede tener dos estados posibles: ya sea que haya tokens guardados en el sesión actual, o bien debes obtener tokens del servidor de OAuth 2.0. Realizar una llamada a la API de prueba si hay tokens en la sesión o pedirle al usuario para acceder.

Python

Abre el archivo routes.py. Primero, establezca algunas constantes 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",
)

Desplázate a la ruta de destino del complemento (en el ejemplo, esta es /classroom-addon) . Agrega lógica para procesar una página de acceso si la sesión no contiene las “credenciales” .

@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 para esta explicación se puede encontrar en el módulo step_02_sign_in.

Abre el archivo application.properties y agrega la configuración de la sesión que sigue 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 manejar la lógica detrás de los endpoints en el archivo del controlador y configurar el URI de redireccionamiento, la ubicación del archivo de secretos del cliente y los alcances la infraestructura y los procesos que requiere la administración de datos. El URI de redireccionamiento se usa para redirigir a los usuarios a un URI específico después de que autoricen tu app. Consulta la sección Configuración del proyecto del README.md en el código fuente para obtener información sobre dónde colocar el client_secret.json archivo.

@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 step_02_sign_in módulo) y agrega lógica a la ruta de destino para renderizar la página de acceso si el 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 “firme” in”. 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 y redirecciona al usuario a una autenticación URL. Esta URL incluye varios datos, como los alcances solicitada, la ruta de destino para la autorización after y el espacio de nombres ID de cliente. Puedes verlas en esta URL de autorización de muestra.

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 los atributos from_client_secrets_file 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. esta es la ruta a la que pretendes que los usuarios que regrese después de autorizar su aplicación. Se muestra /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. Tienda el state en la sesión; se usa para verificar la autenticidad de la respuesta del servidor más tarde. 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 de la Flow y, luego, úsalo para recuperar la URL de autorización:

  • El método getClientSecrets() lee el archivo de secretos del cliente y construye Un objeto GoogleClientSecrets
  • El método getFlow() crea una instancia de GoogleAuthorizationCodeFlow.
  • El método authorize() usa el objeto GoogleAuthorizationCodeFlow, el state y el URI de redireccionamiento para recuperar la URL de autorización. El parámetro state se usa para verificar la autenticidad de la respuesta. del servidor de autorización. Luego, el método devuelve un mapa con el y el parámetro state.
/** Reads the client secret file downloaded from Google Cloud.
 *   @return GoogleClientSecrets read in from client secret file. */
public GoogleClientSecrets getClientSecrets() throws Exception {
    try {
        InputStream in = SignInApplication.class.getClassLoader()
            .getResourceAsStream(CLIENT_SECRET_FILE);
        if (in == null) {
            throw new FileNotFoundException("Client secret file not found: "
                +   CLIENT_SECRET_FILE);
        }
        GoogleClientSecrets clientSecrets = GoogleClientSecrets
            .load(JSON_FACTORY, new InputStreamReader(in));
        return clientSecrets;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns authorization code flow.
*   @return GoogleAuthorizationCodeFlow object used to retrieve an access
*   token and refresh token for the application.
*   @throws Exception if reading client secrets or building code flow object
*   is unsuccessful.
*/
public GoogleAuthorizationCodeFlow getFlow() throws Exception {
    try {
        GoogleAuthorizationCodeFlow authorizationCodeFlow =
            new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT,
                JSON_FACTORY,
                getClientSecrets(),
                getScopes())
                .setAccessType("offline")
                .build();
        return authorizationCodeFlow;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns a map with the authorization URL, which allows the
*   user to give the app permission to their account, and the state parameter,
*   which is used to prevent cross site request forgery.
*   @return map with authorization URL and state parameter.
*   @throws Exception if building the authorization URL is unsuccessful.
*/
public HashMap authorize() throws Exception {
    HashMap<String, String> authDataMap = new HashMap<>();
    try {
        String state = new BigInteger(130, new SecureRandom()).toString(32);
        authDataMap.put("state", state);

        GoogleAuthorizationCodeFlow flow = getFlow();
        String authUrl = flow
            .newAuthorizationUrl()
            .setState(state)
            .setRedirectUri(REDIRECT_URI)
            .build();
        String url = authUrl;
        authDataMap.put("url", url);

        return authDataMap;
    } catch (Exception e) {
        throw e;
    }
}

Usa la inyección de constructor para crear una instancia de la clase de servicio en la 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 el método authorize() de AuthService para recuperar el parámetro state y la URL de autorización. Luego, el extremo almacena state parámetro de 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 regresa a la ruta redirect_uri desde el paso anterior. En el ejemplo anterior, esta ruta es /callback.

Recibirás una code en la respuesta cuando el usuario regrese del página de autorización. Luego, intercambia el código por tokens de acceso y actualización:

Python

Agrega las siguientes importaciones al archivo del servidor de Flask.

import google.oauth2.credentials
import googleapiclient.discovery

Agrega la ruta a tu servidor. Construye otra instancia de google_auth_oauthlib.flow.Flow, pero esta vez vuelve a usar el estado guardado en 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 acceso y tokens de actualización. Por suerte, el objeto flow también contiene el método fetch_token para lograrlo. El método espera los argumentos code o authorization_response. Usa el authorization_response, ya que es la URL completa de la solicitud.

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

Ya tienes las credenciales completas. Guárdalos en la sesión para que se puede recuperar en otros métodos o rutas y, luego, redireccionar a un complemento la página de destino.

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 muestre el objeto Credentials. Para ello, haz lo siguiente: y pasar el código de autorización recuperado del redireccionamiento que realizó la URL de autorización. Este objeto Credentials se usará más tarde para recuperar el token de acceso y el token 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 el código de autorización y el parámetro state de la solicitud. Comparar esto El parámetro state al atributo state almacenado en la sesión Si y, luego, 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 el código de autorización como parámetro. Después de recuperar Credentials de ese objeto, almacénalo en la sesión. Luego, cierra el cuadro de diálogo y redirecciona el 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

Una vez completado el flujo, podrás emitir llamadas a las APIs de Google.

A modo de ejemplo, solicita la información de perfil del usuario. Puedes solicitar el la información del usuario desde la API de OAuth 2.0.

Python

Lee la documentación sobre API de descubrimiento de OAuth 2.0 Úsalo 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 usando 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

Puedes "borrar" las credenciales de un usuario quitándolo 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 la cuenta antes y redireccionarlos a la página de destino del complemento. Tu app debe pasar por de autorización para obtener credenciales nuevas, pero no se solicita a los usuarios que vuelve 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")

También puedes usar flask.session.clear(), pero es posible que esta acción haya sido involuntaria. efectos 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);
    }
}

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 nombre del usuario token de acceso.

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

Agregar un extremo, /revoke, al controlador que borre la sesión y redirecciona al usuario a la página de autorización si se realizó la revocación y exitoso.

/** 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 Teacher Navega a la pestaña Trabajo en clase y haz lo siguiente: Crea una Tarea nueva. 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 la URI de configuración de archivos adjuntos que especificaste en la app del SDK de GWM. Configuración .

¡Felicitaciones! Ahora puedes continuar con el siguiente paso: controlar la repetición visitas a tu complemento.