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:
- Solicite autorização. Forneça um URL de callback como parte dessa solicitação. Quando terminar, você vai receber um URL de autorização.
- 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.
- 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.
- 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 objetoGoogleClientSecrets
. - O método
getFlow()
cria uma instância deGoogleAuthorizationCodeFlow
. - O método
authorize()
usa o objetoGoogleAuthorizationCodeFlow
, astate
e 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 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.