הכנסת המשתמש

זהו המדריך השני בסדרת המדריכים בנושא תוספים ל-Classroom.

במדריך הזה נסביר איך להוסיף כניסה באמצעות חשבון Google לאפליקציית האינטרנט. זוהי התנהגות חובה בתוספים ל-Classroom. משתמשים בפרטי הכניסה מתהליך ההרשאה הזה לכל הקריאות העתידיות ל-API.

במהלך ההסבר המפורט הזה תבצעו את הפעולות הבאות:

  • מגדירים את אפליקציית האינטרנט כך שנתוני הסשן יישמרו ב-iframe.
  • מטמיעים את תהליך הכניסה משרת-אל-שרת באמצעות Google OAuth 2.0.
  • שליחת קריאה ל-OAuth 2.0 API.
  • ליצור מסלולים נוספים כדי לתמוך בהרשאה, ביציאה ובבדיקה של קריאות ל-API.

אחרי שתסיימו, תוכלו לאשר למשתמשים גישה מלאה לאפליקציית האינטרנט שלכם ולבצע קריאות ל-Google APIs.

הסבר על תהליך ההרשאה

ממשקי Google API משתמשים בפרוטוקול OAuth 2.0 לאימות ולהרשאה. תיאור מלא של הטמעת OAuth של Google זמין במדריך בנושא זהויות ב-Google.

פרטי הכניסה של האפליקציה מנוהלים ב-Google Cloud. אחרי שיוצרים אותם, צריך להטמיע תהליך בן ארבעה שלבים כדי לאמת משתמש ולהעניק לו הרשאה:

  1. בקשת הרשאה. צריך לספק כתובת URL לקריאה חוזרת (callback) כחלק מהבקשה הזו. בסיום, תקבלו כתובת URL להרשאה.
  2. הפניה אוטומטית של המשתמש לכתובת ה-URL של ההרשאה. בדף שמוצג למשתמש מפורטות ההרשאות שהאפליקציה דורשת, והוא מתבקש לאשר את הגישה. בסיום, המשתמש מועבר לכתובת ה-URL לקריאה חוזרת.
  3. מקבלים קוד הרשאה בנתיב הקריאה החוזרת. מחליפים את קוד ההרשאה באסימון גישה ובאסימון רענון.
  4. שליחת קריאות ל-Google API באמצעות האסימונים.

קבלת פרטי כניסה של OAuth 2.0

מוודאים שיצרתם והורדתם פרטי כניסה ל-OAuth כפי שמתואר בדף הסקירה הכללית. הפרויקט צריך להשתמש בפרטי הכניסה האלה כדי שהמשתמש יוכל להיכנס.

הטמעה של תהליך ההרשאה

מוסיפים לאפליקציית האינטרנט לוגיקה ומסלולים כדי ליישם את התהליך שמתואר, כולל התכונות הבאות:

  • מתחילים את תהליך ההרשאה כשמגיעים לדף הנחיתה.
  • לבקש הרשאה ולטפל בתגובה של שרת ההרשאות.
  • מנקים את פרטי הכניסה המאוחסנים.
  • מבטלים את ההרשאות של האפליקציה.
  • בדיקת קריאה ל-API.

הפעלת תהליך ההרשאה

במקרה הצורך, משנים את דף הנחיתה כדי להתחיל את תהליך ההרשאה. יש שני מצבים אפשריים לתוסף: או שיש אסימונים שמורים בסשן הנוכחי, או שצריך לקבל אסימונים משרת OAuth 2.0. מבצעים קריאה לבדיקה של ה-API אם יש טוקנים בסשן, או מבקשים מהמשתמש להיכנס לחשבון.

Python

פותחים את קובץ ה-routes.py. קודם מגדירים כמה קבועים ואת הגדרות קובצי ה-Cookie בהתאם להמלצות האבטחה של 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",
)

עוברים למסלול הנחיתה של התוסף (במקרה הזה /classroom-addon בקובץ לדוגמה). מוסיפים לוגיקה לעיבוד דף כניסה אם הסשן לא מכיל את המפתח '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

הקוד של ההדרכה הזו נמצא במודול step_02_sign_in.

פותחים את קובץ application.properties ומוסיפים הגדרת סשן בהתאם להמלצות האבטחה של 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

יוצרים מחלקה של שירות (AuthService.java במודול step_02_sign_in) כדי לטפל בלוגיקה שמאחורי נקודות הקצה בקובץ הבקרה, ומגדירים את ה-URI להפניה אוטומטית, את המיקום של קובץ הסודות של הלקוח ואת ההיקפים שהתוסף דורש. כתובת ה-URI להפניה אוטומטית משמשת להפניה מחדש של המשתמשים לכתובת URI ספציפית אחרי שהם מאשרים את האפליקציה. מידע על המיקום שבו צריך להציב את הקובץ client_secret.json מופיע בקטע Project Set Up (הגדרת הפרויקט) במאמר README.md בקוד המקור.

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

פותחים את קובץ הבקרה (AuthController.java במודול step_02_sign_in) ומוסיפים לוגיקה לנתיב הנחיתה כדי להציג את דף הכניסה אם הסשן לא מכיל את המפתח 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);
    }
}

בדף ההרשאה צריך להיות קישור או לחצן שמאפשרים למשתמש "להיכנס". לחיצה על הלחצן הזה אמורה להפנות את המשתמש אל המסלול authorize.

.

בקשת הרשאה

כדי לבקש הרשאה, צריך ליצור כתובת URL לאימות ולהפנות אליה את המשתמש. כתובת ה-URL הזו כוללת כמה פרטים, כמו היקפי הגישה המבוקשים, נתיב היעד אחרי ההרשאה ומזהה הלקוח של אפליקציית האינטרנט. אפשר לראות אותן בדוגמה הזו של כתובת URL לאישור.

Python

מוסיפים את הייבוא הבא לקובץ routes.py.

import google_auth_oauthlib.flow

יוצרים מסלול חדש /authorize. יוצרים מופע של google_auth_oauthlib.flow.Flow. מומלץ מאוד להשתמש בשיטה from_client_secrets_file שכלולה ב-API כדי לעשות זאת.

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

מגדירים את flow's redirect_uri; זהו הנתיב שאליו רוצים שהמשתמשים יחזרו אחרי שהם מאשרים את האפליקציה. זהו /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)

משתמשים באובייקט של התהליך כדי ליצור את authorization_url ו-state. מאחסנים את state בסשן. הוא ישמש בהמשך לאימות האותנטיות של תגובת השרת. לבסוף, מפנים את המשתמש לכתובת 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

מוסיפים את השיטות הבאות לקובץ AuthService.java כדי ליצור מופע של אובייקט הזרימה, ואז משתמשים בו כדי לאחזר את כתובת ה-URL של ההרשאה:

  • ‫method getClientSecrets() קורא את קובץ הסוד של הלקוח ויוצר אובייקט GoogleClientSecrets.
  • השיטה getFlow() יוצרת מופע של GoogleAuthorizationCodeFlow.
  • השיטה authorize() משתמשת באובייקט GoogleAuthorizationCodeFlow, בפרמטר state וב-URI של ההפניה האוטומטית כדי לאחזר את כתובת ה-URL של ההרשאה. הפרמטר state משמש לאימות האותנטיות של התגובה משרת ההרשאות. השיטה מחזירה מפה עם כתובת ה-URL של ההרשאה והפרמטר 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;
    }
}

משתמשים בהזרקת בנאי כדי ליצור מופע של מחלקת השירות במחלקת הבקר.

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

מוסיפים את נקודת הקצה /authorize למחלקת הבקר. נקודת הקצה הזו קוראת לשיטה AuthService authorize() כדי לאחזר את הפרמטר state ואת כתובת ה-URL של ההרשאה. לאחר מכן, נקודת הקצה מאחסנת את הפרמטר state בסשן ומפנה את המשתמשים לכתובת ה-URL של ההרשאה.

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

טיפול בתגובת השרת

אחרי מתן ההרשאה, המשתמש חוזר לנתיב redirect_uri מהשלב הקודם. בדוגמה הקודמת, המסלול הזה הוא /callback.

תקבלו code בתשובה כשהמשתמש יחזור מדף ההרשאה. לאחר מכן מחליפים את הקוד באסימוני גישה ורענון:

Python

מוסיפים את הייבוא הבא לקובץ השרת של Flask.

import google.oauth2.credentials
import googleapiclient.discovery

מוסיפים את המסלול לשרת. יוצרים עוד מופע של google_auth_oauthlib.flow.Flow, אבל הפעם משתמשים מחדש במצב שנשמר בשלב הקודם.

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

לאחר מכן, מבקשים אסימוני גישה ורענון. למזלנו, האובייקט flow כולל גם את השיטה fetch_token, שמאפשרת לעשות את זה. השיטה מצפה לארגומנטים code או authorization_response. צריך להשתמש ב-authorization_response, כי זו כתובת ה-URL המלאה מהבקשה.

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

עכשיו יש לך אישורים מלאים. מאחסנים אותם בסשן כדי שאפשר יהיה לאחזר אותם בשיטות או בנתיבים אחרים, ואז מבצעים הפניה אוטומטית לדף נחיתה של תוסף.

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

מוסיפים method למחלקת השירות שמחזירה את האובייקט Credentials על ידי העברת קוד ההרשאה שאוחזר מההפניה שבוצעה על ידי כתובת ה-URL של ההרשאה. אובייקט Credentials הזה משמש בהמשך לאחזור אסימון הגישה ואסימון הרענון.

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

מוסיפים נקודת קצה ל-URI של ההפניה לבקר. מאחזרים את קוד ההרשאה ואת הפרמטר state מהבקשה. משווים את הפרמטר state למאפיין state שמאוחסן בסשן. אם הם תואמים, ממשיכים בתהליך ההרשאה. אם הם לא תואמים, הפונקציה מחזירה שגיאה.

לאחר מכן, קוראים לשיטה AuthService getAndSaveCredentials ומעבירים את קוד ההרשאה כפרמטר. אחרי שמקבלים את אובייקט Credentials מאחסנים אותו בסשן. לאחר מכן, סוגרים את תיבת הדו-שיח ומפנים את המשתמש לדף הנחיתה של התוסף.

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

בדיקת קריאה ל-API

אחרי שהתהליך מסתיים, אפשר להפעיל קריאות לממשקי Google API.

לדוגמה, בקשה לקבלת פרטי הפרופיל של המשתמש. אפשר לבקש את פרטי המשתמש מ-OAuth 2.0 API.

Python

קוראים את המסמכים של OAuth 2.0 discovery API ומשתמשים בו כדי לקבל אובייקט UserInfo עם נתונים.

# 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

יוצרים שיטה במחלקת השירות שבונה אובייקט UserInfo באמצעות Credentials כפרמטר.

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

מוסיפים את נקודת הקצה /test לבקר שמציג את כתובת האימייל של המשתמש.

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

ניקוי פרטי כניסה

אפשר 'לנקות' את פרטי הכניסה של משתמש על ידי הסרתם מהסשן הנוכחי. כך תוכלו לבדוק את הניתוב בדף הנחיתה של התוסף.

מומלץ להציג אינדיקציה לכך שהמשתמש התנתק לפני שמפנים אותו לדף הנחיתה של התוסף. האפליקציה צריכה לעבור את תהליך ההרשאה כדי לקבל פרטי כניסה חדשים, אבל המשתמשים לא מתבקשים להעניק לאפליקציה הרשאה מחדש.

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

אפשר גם להשתמש ב-flask.session.clear(), אבל יכול להיות שזה ישפיע על ערכים אחרים שמאוחסנים בסשן.

Java

בבקר, מוסיפים נקודת קצה של /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);
    }
}

ביטול ההרשאה של האפליקציה

משתמש יכול לבטל את ההרשאה שניתנה לאפליקציה שלכם על ידי שליחת POST בקשה אל https://oauth2.googleapis.com/revoke. הבקשה צריכה לכלול את אסימון הגישה של המשתמש.

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

מוסיפים שיטה למחלקת השירות שמבצעת קריאה לנקודת הקצה (endpoint) של ביטול ההרשאה.

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

מוסיפים נקודת קצה, /revoke, לבקר שמנקה את הסשן ומפנה את המשתמש לדף ההרשאה אם הביטול הצליח.

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

בדיקת התוסף

נכנסים אל Google Classroom בתור אחד ממשתמשי הבדיקה מסוג מורה. עוברים לכרטיסייה עבודות ויוצרים מטלה חדשה. לוחצים על הלחצן תוספים מתחת לאזור הטקסט, ואז בוחרים את התוסף הרצוי. ה-iframe נפתח והתוסף טוען את ה-URI של הגדרת הקובץ המצורף שציינתם בדף App Configuration (הגדרת האפליקציה) ב-GWM SDK.

מעולה! אפשר לעבור לשלב הבא: טיפול בביקורים חוזרים בתוסף.