Fazer login do usuário

Este é o segundo tutorial da série de complementos do Google Sala de Aula.

Neste tutorial, você vai adicionar o Login do Google ao aplicativo da Web. Esse é um comportamento obrigatório para complementos do Google Sala de Aula. Use as credenciais desse fluxo de autorização para todas as chamadas futuras para a API.

Durante este tutorial, você vai:

  • Configure seu app da Web para manter os dados da sessão em um iframe.
  • Implemente o fluxo de login de servidor para servidor do Google OAuth 2.0.
  • Faça uma chamada para a API OAuth 2.0.
  • Crie rotas adicionais para oferecer suporte à autorização, ao logout e ao teste de chamadas de API.

Depois disso, você pode autorizar totalmente os usuários no seu app da Web e emitir chamadas para as APIs do Google.

Entender o fluxo de autorização

As APIs do Google usam o protocolo OAuth 2.0 para autenticação e autorização. A descrição completa da implementação do OAuth do Google está disponível no Guia do OAuth do Google Identity.

As credenciais do seu aplicativo são gerenciadas no Google Cloud. Depois que eles forem criados, implemente um processo de quatro etapas para autenticar e autorizar um usuário:

  1. Solicite autorização. Forneça um URL de callback como parte dessa solicitação. Quando terminar, você vai receber um URL de autorização.
  2. Redirecione o usuário para o URL de autorização. A página resultante informa ao usuário as permissões necessárias para o app e solicita que ele permita o acesso. Quando a chamada é concluída, o usuário é direcionado para o URL de callback.
  3. Receba um código de autorização na rota de callback. Troque o código de autorização por um token de acesso e um token de atualização.
  4. Faça chamadas para uma API do Google usando os tokens.

Receber credenciais do OAuth 2.0

Verifique se você criou e fez o download das credenciais do OAuth conforme descrito na página de visão geral. Seu projeto precisa usar essas credenciais para fazer login do usuário.

Implementar o fluxo de autorização

Adicione lógica e rotas ao app da Web para realizar o fluxo descrito, incluindo estes recursos:

  • Inicie o fluxo de autorização ao acessar a página de destino.
  • Solicite autorização e processe a resposta do servidor de autorização.
  • Limpe as credenciais armazenadas.
  • Revogue as permissões do app.
  • Teste uma chamada de API.

Iniciar autorização

Modifique a página de destino para iniciar o fluxo de autorização, se necessário. O complemento pode estar em dois estados possíveis: ou há tokens salvos na sessão atual ou é necessário receber tokens do servidor OAuth 2.0. Faça uma chamada de API de teste se houver tokens na sessão ou solicite que o usuário faça login.

Python

Abra o arquivo routes.py. Primeiro, defina algumas constantes e nossa configuração de cookies de acordo com as recomendações de segurança de 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",
)

Mova para a rota de destino do complemento (/classroom-addon no arquivo de exemplo). Adicione uma lógica para renderizar uma página de login se a sessão não conter a chave "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

O código para este tutorial pode ser encontrado no módulo step_02_sign_in.

Abra o arquivo application.properties e adicione a configuração de sessão que segue as recomendações de segurança 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

Crie uma classe de serviço (AuthService.java no módulo step_02_sign_in) para processar a lógica por trás dos endpoints no arquivo do controlador e configurar o URI de redirecionamento, o local do arquivo de segredos do cliente e os escopos necessários para o complemento. O URI de redirecionamento é usado para redirecionar os usuários para um URI específico depois que eles autorizam o app. Consulte a seção "Configuração do projeto" do README.md no código-fonte para saber onde colocar o arquivo 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));
    }
}

Abra o arquivo do controlador (AuthController.java no módulo step_02_sign_in) e adicione lógica à rota de destino para renderizar a página de login se a sessão não contiver a chave 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);
    }
}

Sua página de autorização precisa conter um link ou botão para o usuário "fazer login". Clicar nele redireciona o usuário para a rota authorize.

Solicitar autorização

Para solicitar autorização, crie e redirecione o usuário a um URL de autenticação. Esse URL inclui várias informações, como os escopos solicitados, a rota de destino para a autorização após e o ID do cliente do app da Web. Confira esses valores neste exemplo de URL de autorização.

Python

Adicione a importação a seguir ao arquivo routes.py.

import google_auth_oauthlib.flow

Crie uma nova rota /authorize. Crie uma instância de google_auth_oauthlib.flow.Flow. Recomendamos o uso do método from_client_secrets_file incluído para fazer isso.

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

Defina o redirect_uri do flow. Essa é a rota para a qual você quer que os usuários voltem depois de autorizar seu app. Esse é o /callback no exemplo a seguir.

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

Use o objeto de fluxo para construir o authorization_url e o state. Armazene o state na sessão. Ele é usado para verificar a autenticidade da resposta do servidor mais tarde. Por fim, redirecione o usuário para o 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

Adicione os seguintes métodos ao arquivo AuthService.java para instanciar o objeto de fluxo e usá-lo para recuperar o URL de autorização:

  • O método getClientSecrets() lê o arquivo da chave secreta do cliente e cria um objeto GoogleClientSecrets.
  • O método getFlow() cria uma instância de GoogleAuthorizationCodeFlow.
  • O método authorize() usa o objeto GoogleAuthorizationCodeFlow, o parâmetro state e o URI de redirecionamento para recuperar o URL de autorização. O parâmetro state é usado para verificar a autenticidade da resposta do servidor de autorização. O método retorna um mapa com o URL de autorização e o 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;
    }
}

Use a injeção de construtor para criar uma instância da classe de serviço na classe do 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;
}

Adicione o endpoint /authorize à classe do controlador. Esse endpoint chama o método authorize() do AuthService para recuperar o parâmetro state e o URL de autorização. Em seguida, o endpoint armazena o parâmetro state na sessão e redireciona os usuários para o URL de autorização.

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

Processar a resposta do servidor

Após a autorização, o usuário retorna à rota redirect_uri da etapa anterior. No exemplo anterior, essa rota é /callback.

Você recebe um code na resposta quando o usuário retorna da página de autorização. Em seguida, troque o código por tokens de acesso e de atualização:

Python

Adicione as importações a seguir ao arquivo do servidor Flask.

import google.oauth2.credentials
import googleapiclient.discovery

Adicione o trajeto ao servidor. Crie outra instância de google_auth_oauthlib.flow.Flow, mas desta vez reutilize o estado salvo na etapa 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)

Em seguida, solicite tokens de acesso e de atualização. Felizmente, o objeto flow também contém o método fetch_token para fazer isso. O método espera os argumentos code ou authorization_response. Use o authorization_response, porque ele é o URL completo da solicitação.

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

Agora você tem as credenciais completas. Armazene-os na sessão para que eles possam ser recuperados em outros métodos ou rotas e redirecione para uma página de destino de 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

Adicione um método à classe de serviço que retorne o objeto Credentials transmitindo o código de autorização recuperado do redirecionamento realizado pelo URL de autorização. Esse objeto Credentials é usado mais tarde para recuperar o token de acesso e o token de atualização.

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

Adicione um endpoint para o URI de redirecionamento ao controlador. Extraia o código de autorização e o parâmetro state da solicitação. Compare esse parâmetro state com o atributo state armazenado na sessão. Se eles forem correspondentes, continue com o fluxo de autorização. Se eles não corresponderem, retorne um erro.

Em seguida, chame o método getAndSaveCredentials AuthService e transmita o código de autorização como um parâmetro. Depois de extrair o objeto Credentials, armazene-o na sessão. Em seguida, feche a caixa de diálogo e redirecione o usuário para a página de destino do 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);
    }
}

Testar uma chamada de API

Com o fluxo concluído, agora você pode emitir chamadas para as APIs do Google.

Por exemplo, solicite as informações do perfil do usuário. É possível solicitar as informações do usuário na API OAuth 2.0.

Python

Leia a documentação da API de descoberta OAuth 2.0 e use-a para receber um objeto UserInfo preenchido.

# 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

Crie um método na classe de serviço que crie um objeto UserInfo usando o 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;
    }
}

Adicione o endpoint /test ao controlador que mostra o e-mail do usuário.

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

Limpar credenciais

Você pode "limpar" as credenciais de um usuário removendo-as da sessão atual. Assim, você pode testar o roteamento na página de destino do complemento.

Recomendamos mostrar uma indicação de que o usuário fez logout antes de redirecionar para a página de destino do complemento. O app precisa passar pelo fluxo de autorização para receber novas credenciais, mas os usuários não são solicitados a autorizar o app novamente.

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, use flask.session.clear(), mas isso pode ter efeitos indesejados se você tiver outros valores armazenados na sessão.

Java

No controlador, adicione um endpoint /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);
    }
}

Revogar a permissão do app

Um usuário pode revogar a permissão do app enviando uma solicitação POST para https://oauth2.googleapis.com/revoke. A solicitação precisa conter o token de acesso do usuário.

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

Adicione um método à classe de serviço que faça uma chamada para o endpoint de revogação.

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

Adicione um endpoint, /revoke, ao controlador que limpa a sessão e redireciona o usuário para a página de autorização se a revogação tiver sido concluída.

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

Testar o complemento

Faça login no Google Sala de Aula como um dos usuários de teste Professor. Navegue até a guia Atividades e crie uma nova Atividade. Clique no botão Complementos abaixo da área de texto e selecione o complemento. O iframe é aberto e o complemento carrega o URI de configuração de anexo que você especificou na página Configuração do app do SDK do GWM.

Parabéns! Você está pronto para seguir para a próxima etapa: processar visitas repetidas ao seu complemento.