Fazer login do usuário

Este é o segundo tutorial sobre os complementos do Google Sala de Aula série de tutoriais.

Neste tutorial, você vai adicionar o Login do Google ao aplicativo da Web. Esta é uma comportamento necessário para os complementos do Google Sala de Aula. Use as credenciais de fluxo de autorização para todas as chamadas futuras à API.

Neste tutorial, você vai fazer o seguinte:

  • Configure seu app da Web para manter os 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.
  • Criar outras rotas para autorizar, sair e testar chamadas de API.

Depois de terminar, você pode autorizar totalmente os usuários no seu aplicativo da Web e emitir chamadas para nas 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 de OAuth do Google está disponível no Guia do OAuth do Google Identity.

As credenciais do aplicativo são gerenciadas no Google Cloud. Assim que tiverem foi criado, implementam um processo de quatro etapas para autenticar e autorizar uma 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 usuário das permissões que seu aplicativo exige e solicita que ele conceda o acesso. Quando concluído, o usuário é encaminhado para o URL de retorno de chamada.
  3. Receber um código de autorização na sua rota de callback. Troque o código de autorização para 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 OAuth, conforme descrito em a Página de visão geral. Seu projeto precisa usar essas credenciais para fazer o login do usuário.

Implementar o fluxo de autorização

Adicionar lógica e rotas ao nosso app da Web para realizar o fluxo descrito, 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.
  • Revogar as permissões do app.
  • Teste uma chamada de API.
.

Iniciar autorização

Modifique sua página de destino para iniciar o fluxo de autorização, se necessário. O pode ter dois estados possíveis: se há tokens salvos sessão atual ou precisar de tokens do servidor OAuth 2.0. Realizar fazer uma chamada de API de teste se houver tokens na sessão ou perguntar ao usuário para fazer login.

Python

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

Vá para sua rota de destino complementar (no exemplo, /classroom-addon) ). Adicionar lógica para renderizar uma página de login se a sessão não contiver as "credenciais" de dados.

@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 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 lidar com a lógica por trás dos endpoints no arquivo do controlador e configurar o URI de redirecionamento, o local do arquivo das chaves secretas do cliente e os escopos do seu complemento exige. O URI de redirecionamento é usado para redirecionar os usuários a um URI específico depois de autorizar o app. Consulte a seção "Configuração do projeto" README.md no código-fonte para saber onde colocar os 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 step_02_sign_in) ) e adicionar lógica à rota de destino para renderizar a página de login se o a sessão não contém a tecla 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 deve conter um link ou botão para que o usuário "assine pol". Clicar nela deve redirecionar o usuário para a rota authorize.

Solicitar autorização

Para solicitar a autorização, construa e redirecione o usuário para um modelo URL. Esse URL inclui várias informações, como os escopos solicitada, a rota de destino para a autorização após a autorização e o método ID do cliente. Confira as instruções 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; é altamente recomendável usar from_client_secrets_file 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 da flow. esse é o trajeto para que os usuários seja retornado após autorizar o app. Este é /callback no seguinte exemplo.

# 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. Armazenamento state na sessão. ele é usado para verificar a autenticidade a uma resposta do servidor. Por fim, redirecione o usuário para 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 recuperar 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, a 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. Em seguida, 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 no Controller.

/** 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 de controlador. Esse endpoint chama o método authorize() do AuthService para recuperar o parâmetro state; e o URL de autorização. Depois, o endpoint armazena o 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 anterior, esse trajeto é /callback.

Você receberá um code na resposta quando o usuário retornar do página de autorização. Em seguida, troque o código por tokens de acesso e 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 reutilizam o estado salvo no 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 os 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 é o URL completo da solicitação.

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

Agora você tem credenciais completas. Armazene-os na sessão para que eles podem ser recuperados em outros métodos ou rotas e redirecionar para um complemento 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

Adicione um método à classe de serviço que retorne o objeto Credentials: passando o código de autorização recuperado do redirecionamento realizado pelo o URL de autorização. Esse objeto Credentials é usado mais tarde para recuperar o token 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 ao URI de redirecionamento ao controlador. Recupere o código de autorização e o parâmetro state da solicitação. Comparar isso o parâmetro state para o atributo state armazenado na sessão. Se eles e 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 como parâmetro. Depois de recuperar Credentials objeto, armazene-o na sessão. Em seguida, feche a caixa de diálogo e redirecione o o usuário à 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 é possível emitir chamadas para as APIs do Google.

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

Python

Leia a documentação API de descoberta do OAuth 2.0 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, é possível testar o roteamento na página de destino do complemento.

Recomendamos mostrar uma indicação de que o usuário já saiu da sua conta redirecionando-as para a página de destino do complemento. Seu aplicativo deve passar pelo fluxo de autorização para obter novas credenciais, mas os usuários não são solicitados Autorize 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")

Você também pode usar flask.session.clear(), mas isso pode não acontecer se outros valores estiverem 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 nome token de acesso.

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 bem-sucedido.

/** 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 no teste do Professor. Acesse a guia Atividades e crie uma nova Atividade. Clique no botão Complementos abaixo da área de texto. e selecione seu complemento. O iframe é aberto e o complemento carrega o URI de configuração de anexos que você especificou no app do SDK do GWM Configuração página.

Parabéns! Você já pode avançar para a próxima etapa: como lidar com a repetição visitas ao complemento.