Este é o segundo tutorial da série de tutoriais sobre 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.
Ao longo deste tutorial, você vai concluir as seguintes etapas:
- Configurar o app da Web para manter os dados da sessão em um iframe.
- Implementar o fluxo de login do servidor para o servidor do OAuth 2.0 do Google.
- Fazer uma chamada para a API OAuth 2.0.
- Criar outras rotas para oferecer suporte à autorização, ao logout e ao teste de chamadas de API.
Quando terminar, você poderá autorizar totalmente os usuários no app da Web e fazer 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 aplicativo são gerenciadas no Google Cloud. Depois de criadas, implemente um processo de quatro etapas para autenticar e autorizar um usuário:
- Solicitar autorização. Forneça um URL de callback como parte dessa solicitação. Quando concluído, você recebe um URL de autorização.
- 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 pede que ele permita o acesso. Quando concluído, o usuário é encaminhado para o URL de callback.
- 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.
- 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 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:
- Iniciar o fluxo de autorização ao acessar a página de destino.
- Solicitar autorização e processar a resposta do servidor de autorização.
- Limpar as credenciais armazenadas.
- Revogar as permissões do app.
- Testar 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: há tokens salvos na sessão atual ou você precisa receber tokens do servidor OAuth 2.0. Faça uma chamada de API de teste se houver tokens na sessão ou peça ao usuário para fazer 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 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",
)
Acesse a rota de destino do complemento (que é /classroom-addon no arquivo de exemplo). Adicione lógica para renderizar uma página de login se a sessão não contiver
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 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 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 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 step_02_sign_in
módulo) 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);
}
}
A página de autorização precisa conter um link ou botão para o usuário "fazer login". Ao clicar nele, 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 após a autorização e o ID do cliente do app da Web. Você pode conferir essas informações neste URL de autorização de amostra.
Python
Adicione a seguinte importação 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 usar o 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ê pretende que os usuários retornem depois de autorizar o app. Esse é /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 criar 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 de chaves secretas do cliente e cria um objetoGoogleClientSecrets. - O método
getFlow()cria uma instância deGoogleAuthorizationCodeFlow. - O método
authorize()usa o objetoGoogleAuthorizationCodeFlow, o parâmetrostatee o URI de redirecionamento para recuperar o URL de autorização. O parâmetrostateé 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âmetrostate.
/** Reads the client secret file downloaded from Google Cloud.
* @return GoogleClientSecrets read in from client secret file. */
public GoogleClientSecrets getClientSecrets() throws Exception {
try {
InputStream in = SignInApplication.class.getClassLoader()
.getResourceAsStream(CLIENT_SECRET_FILE);
if (in == null) {
throw new FileNotFoundException("Client secret file not found: "
+ CLIENT_SECRET_FILE);
}
GoogleClientSecrets clientSecrets = GoogleClientSecrets
.load(JSON_FACTORY, new InputStreamReader(in));
return clientSecrets;
} catch (Exception e) {
throw e;
}
}
/** Builds and returns authorization code flow.
* @return GoogleAuthorizationCodeFlow object used to retrieve an access
* token and refresh token for the application.
* @throws Exception if reading client secrets or building code flow object
* is unsuccessful.
*/
public GoogleAuthorizationCodeFlow getFlow() throws Exception {
try {
GoogleAuthorizationCodeFlow authorizationCodeFlow =
new GoogleAuthorizationCodeFlow.Builder(
HTTP_TRANSPORT,
JSON_FACTORY,
getClientSecrets(),
getScopes())
.setAccessType("offline")
.build();
return authorizationCodeFlow;
} catch (Exception e) {
throw e;
}
}
/** Builds and returns a map with the authorization URL, which allows the
* user to give the app permission to their account, and the state parameter,
* which is used to prevent cross site request forgery.
* @return map with authorization URL and state parameter.
* @throws Exception if building the authorization URL is unsuccessful.
*/
public HashMap authorize() throws Exception {
HashMap<String, String> authDataMap = new HashMap<>();
try {
String state = new BigInteger(130, new SecureRandom()).toString(32);
authDataMap.put("state", state);
GoogleAuthorizationCodeFlow flow = getFlow();
String authUrl = flow
.newAuthorizationUrl()
.setState(state)
.setRedirectUri(REDIRECT_URI)
.build();
String url = authUrl;
authDataMap.put("url", url);
return authDataMap;
} catch (Exception e) {
throw e;
}
}
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 state
parâmetro 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 seguintes importações ao arquivo do servidor Flask.
import google.oauth2.credentials
import googleapiclient.discovery
Adicione a rota 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 realizar essa ação. O método espera
os argumentos code ou authorization_response. Use o authorization_response, já que ele é o URL completo da solicitação.
authorization_response = flask.request.url
flow.fetch_token(authorization_response=authorization_response)
Agora você tem credenciais completas. Armazene-as na sessão para que possam ser recuperadas em outros métodos ou rotas e, em seguida, redirecione para uma página de destino do 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. 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 eles corresponderem, continue com o fluxo de autorização. Se não corresponderem, retorne um erro.
Em seguida, chame o método getAndSaveCredentials do AuthService 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, agora você pode fazer chamadas para as APIs do Google.
Como exemplo, solicite as informações de perfil do usuário. Você pode solicitar as informações do usuário na API OAuth 2.0.
Python
Leia a documentação da 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 as Credentials como um 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. Isso permite testar o roteamento na página de destino do complemento.
Recomendamos mostrar uma indicação de que o usuário fez logout 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 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 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 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 de configuração do app do SDK do GWM.
Parabéns! Você já pode passar para a próxima etapa: processar visitas repetidas ao complemento.