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 os complementos do Google Sala de Aula. Use as credenciais desse fluxo de autorização para todas as chamadas futuras à API.

Neste tutorial, você vai fazer o seguinte:

  • Configure seu app da Web para manter dados da sessão em um iframe.
  • Implementar o fluxo de login de servidor para servidor do Google OAuth 2.0.
  • Emita uma chamada para a API OAuth 2.0.
  • Crie rotas adicionais para dar suporte à autorização, saída e teste de chamadas de API.

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

Entenda 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 de OAuth do Google Identity.

As credenciais do seu aplicativo são gerenciadas no Google Cloud. Depois de criar, 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. Após a conclusão, você recebe um URL de autorização.
  2. Redirecione o usuário para o URL de autorização. A página resultante informa o usuário sobre as permissões exigidas pelo app e solicita que ele conceda o acesso. Quando concluído, o usuário é encaminhado para o URL de callback.
  3. Receba um código de autorização na sua rota de callback. Troque o código de autorização por um token de acesso e um token de atualização.
  4. Fazer 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 "Visão geral". Seu projeto precisa usar essas credenciais para fazer o login do usuário.

Implementar o fluxo de autorização

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

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

Iniciar autorização

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

Python

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

Vá para a rota de destino do complemento (/classroom-addon no arquivo de exemplo). Adicione lógica para renderizar uma página de login se a sessão não tiver 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 deste tutorial está disponível no módulo step_02_sign_in.

Abra o arquivo application.properties e adicione a configuração da sessão que segue as recomendações de segurança do 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 chaves secretas do cliente e os escopos exigidos pelo complemento. O URI de redirecionamento é usado para redirecionar os usuários a 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 informações sobre 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 tiver 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 que o usuário faça login. Se você clicar nesse botão, o usuário será redirecionado para a rota authorize.

Solicitar autorização

Para solicitar autorização, crie e redirecione o usuário para 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. Elas estão disponíveis 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 a redirect_uri do flow. Essa é a rota que você pretende que os usuários retornem após autorizar o app. No exemplo abaixo, temos /callback.

# 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 authorization_url e 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 use-o para extrair o URL de autorização:

  • O método getClientSecrets() lê o arquivo de 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. Depois, 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

Depois de autorizar, o usuário retorna à rota redirect_uri da etapa anterior. No exemplo acima, essa rota é /callback.

Você vai receber uma code na resposta quando o usuário retornar 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 de servidor Flask.

import google.oauth2.credentials
import googleapiclient.discovery

Adicione a rota ao seu 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, que é 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 possam ser recuperados em outros métodos ou rotas e redirecione a 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 posteriormente para recuperar os tokens de acesso e 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. Recupere 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 forem correspondentes, continue com o fluxo de autorização. Se não corresponderem, retornará um erro.

Em seguida, chame o método AuthService getAndSaveCredentials e transmita o código de autorização como um parâmetro. Depois de recuperar 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, você já 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 pela API OAuth 2.0.

Python

Leia a documentação da API de descoberta do OAuth 2.0. Use-o 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 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 exibe 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 trajeto na página de destino do complemento.

Recomendamos mostrar uma indicação de que o usuário saiu antes de redirecioná-lo 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 poderá ter efeitos não intencionais 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 seu app enviando uma solicitação POST para https://oauth2.googleapis.com/revoke. A solicitação deve 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 for bem-sucedida.

/** 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 de Professor. Acesse 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 do anexo especificado na página Configuração do app do SDK do GWM.

Parabéns! Você já pode prosseguir para a próxima etapa: como gerenciar acessos repetidos ao seu complemento.