Memproses login pengguna

Ini adalah panduan kedua dalam seri panduan add-on Classroom.

Dalam panduan ini, Anda menambahkan Login dengan Google ke aplikasi web. Ini adalah perilaku yang diperlukan untuk add-on Classroom. Gunakan kredensial dari alur otorisasi ini untuk semua panggilan mendatang ke API.

Dalam panduan ini, Anda akan menyelesaikan hal-hal berikut:

  • Konfigurasi aplikasi web Anda untuk mempertahankan data sesi dalam iframe.
  • Terapkan alur login server-ke-server Google OAuth 2.0.
  • Lakukan panggilan ke OAuth 2.0 API.
  • Buat rute tambahan untuk mendukung pemberian otorisasi, logout, dan pengujian panggilan API.

Setelah selesai, Anda dapat sepenuhnya memberikan otorisasi kepada pengguna di aplikasi web Anda dan melakukan panggilan ke Google API.

Memahami alur otorisasi

Google API menggunakan protokol OAuth 2.0 untuk autentikasi dan otorisasi. Deskripsi lengkap implementasi OAuth Google tersedia di panduan Google Identity OAuth.

Kredensial aplikasi Anda dikelola di Google Cloud. Setelah proses ini dibuat, terapkan proses empat langkah untuk mengautentikasi dan memberikan otorisasi kepada pengguna:

  1. Minta otorisasi. Berikan URL callback sebagai bagian dari permintaan ini. Setelah selesai, Anda akan menerima URL otorisasi.
  2. Alihkan pengguna ke URL otorisasi. Halaman yang dihasilkan memberi tahu pengguna tentang izin yang diperlukan aplikasi Anda, dan meminta mereka untuk memberikan akses. Setelah selesai, pengguna dirutekan ke URL callback.
  3. Terima kode otorisasi di rute callback Anda. Tukarkan kode otorisasi dengan token akses dan token refresh.
  4. Lakukan panggilan ke Google API menggunakan token.

Mendapatkan kredensial OAuth 2.0

Pastikan Anda telah membuat dan mendownload kredensial OAuth seperti yang dijelaskan di halaman Ringkasan. Project Anda harus menggunakan kredensial ini untuk memproses login pengguna.

Mengimplementasikan alur otorisasi

Tambahkan logika dan rute ke aplikasi web kami untuk mewujudkan alur yang dijelaskan, termasuk fitur-fitur ini:

  • Mulai alur otorisasi saat membuka halaman landing.
  • Minta otorisasi dan tangani respons server otorisasi.
  • Hapus kredensial yang disimpan.
  • Mencabut izin aplikasi.
  • Menguji panggilan API.

Mulai otorisasi

Ubah halaman landing untuk memulai alur otorisasi jika diperlukan. Add-on dapat memiliki dua kemungkinan status; ada token tersimpan pada sesi saat ini, atau Anda perlu mendapatkan token dari server OAuth 2.0. Lakukan panggilan API pengujian jika ada token dalam sesi, atau minta pengguna untuk login.

Python

Buka file routes.py Anda. Pertama, tetapkan beberapa konstanta dan konfigurasi cookie kita sesuai dengan rekomendasi keamanan 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",
)

Beralih ke rute landing add-on Anda (ini adalah /classroom-addon dalam contoh file). Tambahkan logika untuk merender halaman login jika sesi tidak berisi kunci "kredensial".

@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

Kode untuk panduan ini dapat ditemukan di modul step_02_sign_in.

Buka file application.properties, lalu tambahkan konfigurasi sesi yang mengikuti rekomendasi keamanan 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

Buat class layanan (AuthService.java dalam modul step_02_sign_in) untuk menangani logika di balik endpoint dalam file pengontrol dan siapkan URI pengalihan, lokasi file secret klien, dan cakupan yang diperlukan add-on Anda. URI pengalihan digunakan untuk mengalihkan pengguna ke URI tertentu setelah mereka memberikan otorisasi pada aplikasi Anda. Lihat bagian Penyiapan Project pada README.md dalam kode sumber untuk mengetahui informasi tentang lokasi penempatan file 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));
    }
}

Buka file pengontrol (AuthController.java di modul step_02_sign_in) dan tambahkan logika ke rute landing untuk merender halaman login jika sesi tidak berisi kunci 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);
    }
}

Halaman otorisasi Anda harus berisi link atau tombol bagi pengguna untuk "login". Mengklik opsi ini akan mengalihkan pengguna ke rute authorize.

Minta otorisasi

Untuk meminta otorisasi, buat dan alihkan pengguna ke URL autentikasi. URL ini berisi beberapa informasi, seperti cakupan yang diminta, rute tujuan untuk otorisasi setelah otorisasi, dan client ID aplikasi web. Anda dapat melihatnya di contoh URL otorisasi ini.

Python

Tambahkan impor berikut ke file routes.py Anda.

import google_auth_oauthlib.flow

Buat rute baru /authorize. Buat instance google_auth_oauthlib.flow.Flow; sebaiknya gunakan metode from_client_secrets_file yang disertakan untuk melakukannya.

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

Setel redirect_uri flow; ini adalah rute yang akan dituju pengguna untuk ditampilkan setelah memberi otorisasi pada aplikasi Anda. Rute ini adalah /callback dalam contoh berikut.

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

Gunakan objek flow untuk membuat authorization_url dan state. Simpan state dalam sesi; tindakan ini digunakan untuk memverifikasi keaslian respons server nanti. Terakhir, alihkan pengguna ke 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

Tambahkan metode berikut ke file AuthService.java untuk membuat instance objek flow, lalu gunakan metode tersebut untuk mengambil URL otorisasi:

  • Metode getClientSecrets() membaca file rahasia klien dan membuat objek GoogleClientSecrets.
  • Metode getFlow() membuat instance GoogleAuthorizationCodeFlow.
  • Metode authorize() menggunakan objek GoogleAuthorizationCodeFlow, parameter state, dan URI pengalihan untuk mengambil URL otorisasi. Parameter state digunakan untuk memverifikasi keaslian respons dari server otorisasi. Metode ini kemudian menampilkan peta dengan URL otorisasi dan parameter 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;
    }
}

Gunakan injeksi konstruktor untuk membuat instance class layanan di class pengontrol.

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

Tambahkan endpoint /authorize ke class pengontrol. Endpoint ini memanggil metode authorize() AuthService untuk mengambil parameter state dan URL otorisasi. Kemudian, endpoint menyimpan parameter state dalam sesi dan mengalihkan pengguna ke URL otorisasi.

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

Menangani respons server

Setelah memberi otorisasi, pengguna kembali ke rute redirect_uri dari langkah sebelumnya. Pada contoh sebelumnya, rute ini adalah /callback.

Anda akan menerima code dalam respons saat pengguna kembali dari halaman otorisasi. Kemudian tukar kode dengan token akses dan refresh:

Python

Tambahkan impor berikut ke file server Flask Anda.

import google.oauth2.credentials
import googleapiclient.discovery

Tambahkan rute ke server Anda. Buat instance google_auth_oauthlib.flow.Flow lain, tetapi kali ini gunakan kembali status yang disimpan pada langkah sebelumnya.

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

Selanjutnya, minta akses dan token refresh. Untungnya, objek flow juga berisi metode fetch_token untuk melakukannya. Metode ini mengharapkan argumen code atau authorization_response. Gunakan authorization_response, karena merupakan URL lengkap dari permintaan.

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

Sekarang Anda memiliki kredensial yang lengkap. Simpan peristiwa tersebut dalam sesi sehingga dapat diambil di metode atau rute lain, lalu dialihkan ke halaman landing add-on.

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

Tambahkan metode ke class layanan Anda yang menampilkan objek Credentials dengan meneruskan kode otorisasi yang diambil dari pengalihan yang dilakukan oleh URL otorisasi. Objek Credentials ini digunakan nanti untuk mengambil token akses dan token refresh.

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

Tambahkan endpoint untuk URI pengalihan ke pengontrol. Ambil kode otorisasi dan parameter state dari permintaan. Bandingkan parameter state ini dengan atribut state yang disimpan dalam sesi. Jika keduanya cocok, lanjutkan dengan alur otorisasi. Jika tidak cocok, akan menampilkan {i>error<i}.

Selanjutnya, panggil metode AuthService getAndSaveCredentials dan teruskan kode otorisasi sebagai parameter. Setelah mengambil objek Credentials, simpan objek tersebut dalam sesi. Kemudian, tutup dialog dan alihkan pengguna ke halaman landing add-on.

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

Menguji panggilan API

Setelah alurnya selesai, Anda kini dapat melakukan panggilan ke Google API.

Sebagai contoh, minta informasi profil pengguna. Anda dapat meminta informasi pengguna dari OAuth 2.0 API.

Python

Baca dokumentasi untuk OAuth 2.0 discovery API Gunakan untuk mendapatkan objek UserInfo yang telah terisi.

# 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

Buat metode di class layanan yang membuat objek UserInfo menggunakan Credentials sebagai parameter.

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

Tambahkan endpoint /test ke pengontrol yang menampilkan email pengguna.

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

Hapus kredensial

Anda dapat "menghapus" kredensial pengguna dengan menghapusnya dari sesi saat ini. Dengan begitu, Anda dapat menguji perutean di halaman landing add-on.

Sebaiknya tampilkan indikasi bahwa pengguna telah logout sebelum mengalihkan mereka ke halaman landing add-on. Aplikasi Anda harus melalui alur otorisasi untuk mendapatkan kredensial baru, tetapi pengguna tidak diminta untuk memberi otorisasi ulang aplikasi Anda.

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

Atau, gunakan flask.session.clear(), tetapi cara ini mungkin memiliki efek yang tidak diinginkan jika Anda memiliki nilai lain yang disimpan dalam sesi tersebut.

Java

Di pengontrol, tambahkan 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);
    }
}

Mencabut izin aplikasi

Pengguna dapat mencabut izin aplikasi Anda dengan mengirim permintaan POST ke https://oauth2.googleapis.com/revoke. Permintaan harus berisi token akses pengguna.

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

Tambahkan metode ke class layanan yang melakukan panggilan ke endpoint pencabutan.

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

Tambahkan endpoint, /revoke, ke pengontrol yang menghapus sesi dan mengalihkan pengguna ke halaman otorisasi jika pencabutan berhasil.

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

Menguji add-on

Login ke Google Classroom sebagai salah satu pengguna uji coba Pengajar Anda. Pilih tab Tugas Kelas dan buat Tugas baru. Klik tombol Add-ons di bawah area teks, lalu pilih add-on Anda. iframe akan terbuka dan add-on memuat URI Penyiapan Lampiran yang Anda tentukan di halaman Konfigurasi Aplikasi GWM SDK.

Selamat! Anda siap untuk melanjutkan ke langkah berikutnya: menangani kunjungan berulang ke add-on Anda.