הכנסת המשתמש

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

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

במהלך המדריך הזה תלמדו:

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

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

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

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

פרטי הכניסה של האפליקציה מנוהלים ב-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 ספציפית אחרי שהם נותנים הרשאה לאפליקציה. בקטע 'הגדרת הפרויקט' בקובץ README.md בקוד המקור מוסבר איפה צריך למקם את הקובץ 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));
    }
}

פותחים את קובץ הבקר (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 שכלולה ב-SDK.

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

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

  • השיטה 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;
    }
}

משתמשים בהזרקה של מגדיר (constructor) כדי ליצור מופע של סוג השירות בכיתה של הבקר.

/** 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 לכיתה של הבקר. נקודת הקצה הזו מפעילה את השיטה authorize() של AuthService כדי לאחזר את הפרמטר 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

מוסיפים ל-class של השירות 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 APIs.

לדוגמה, אפשר לבקש את פרטי הפרופיל של המשתמש. אפשר לבקש את פרטי המשתמש מ-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

ב-Controller, מוסיפים נקודת קצה מסוג /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 בתור אחד ממשתמשי הבדיקה בתפקיד מורה. עוברים לכרטיסייה עבודות ויוצרים מטלה חדשה. לוחצים על הלחצן Add-ons (תוספים) מתחת לאזור הטקסט ובוחרים את התוסף הרצוי. ה-iframe נפתח והתוסף טוען את URI להגדרת הקובץ המצורף שציינתם בדף הגדרת האפליקציה של GWM SDK.

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