OAuth 2.0 dan Library Klien Google OAuth untuk Java

Ringkasan

Tujuan: Dokumen ini menjelaskan fungsi OAuth 2.0 umum yang ditawarkan oleh Library Klien Google OAuth untuk Java. Anda dapat menggunakan fungsi-fungsi ini untuk otentikasi dan otorisasi untuk semua layanan Internet.

Guna mengetahui petunjuk cara menggunakan GoogleCredential untuk melakukan otorisasi OAuth 2.0 dengan layanan Google, lihat Menggunakan OAuth 2.0 dengan Library Klien Google API untuk Java.

Ringkasan: OAuth 2.0 adalah spesifikasi standar yang memungkinkan pengguna akhir memberikan otorisasi kepada aplikasi klien dengan aman untuk mengakses resource sisi server yang dilindungi. Selain itu, spesifikasi token pemilik OAuth 2.0 menjelaskan cara mengakses resource yang dilindungi tersebut menggunakan token akses yang diberikan selama proses otorisasi pengguna akhir.

Untuk detailnya, lihat dokumentasi Javadoc untuk paket berikut:

Pendaftaran klien

Sebelum menggunakan Library Klien Google OAuth untuk Java, Anda mungkin perlu mendaftarkan aplikasi Anda dengan server otorisasi untuk menerima client ID dan rahasia klien. (Untuk informasi umum tentang proses ini, lihat Spesifikasi Pendaftaran Klien.)

Penyimpanan kredensial dan kredensial

Kredensial adalah class helper OAuth 2.0 yang aman untuk thread untuk mengakses resource yang dilindungi menggunakan token akses. Saat menggunakan token refresh, Credential juga akan memperbarui token akses saat masa berlaku token akses berakhir menggunakan token refresh. Misalnya, jika sudah memiliki token akses, Anda dapat membuat permintaan dengan cara berikut:

  public static HttpResponse executeGet(
      HttpTransport transport, JsonFactory jsonFactory, String accessToken, GenericUrl url)
      throws IOException {
    Credential credential =
        new Credential(BearerToken.authorizationHeaderAccessMethod()).setAccessToken(accessToken);
    HttpRequestFactory requestFactory = transport.createRequestFactory(credential);
    return requestFactory.buildGetRequest(url).execute();
  }

Sebagian besar aplikasi harus mempertahankan token akses dan token refresh kredensial untuk menghindari pengalihan di masa mendatang ke halaman otorisasi di browser. Implementasi CredentialStore di library ini tidak digunakan lagi dan akan dihapus dalam rilis mendatang. Alternatifnya adalah menggunakan antarmuka DataStoreFactory dan DataStore dengan StoredCredential, yang disediakan oleh Library Klien HTTP Google untuk Java.

Anda dapat menggunakan salah satu implementasi berikut yang disediakan oleh library:

Pengguna Google App Engine:

AppEngineCredentialStore tidak digunakan lagi dan sedang dihapus.

Sebaiknya gunakan AppEngineDataStoreFactory dengan StoredCredential. Jika memiliki kredensial yang disimpan dengan cara lama, Anda dapat menggunakan metode bantuan tambahan MigrateTo(AppEngineDataStoreFactory) atau migrationTo(DataStore) untuk melakukan migrasi.

Gunakan DataStoreCredentialRefreshListener dan tetapkan untuk kredensial menggunakan GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener).

Alur kode otorisasi

Gunakan alur kode otorisasi untuk memungkinkan pengguna akhir memberi aplikasi Anda akses ke data mereka yang dilindungi. Protokol untuk alur ini ditentukan dalam spesifikasi Pemberian Kode Otorisasi.

Alur ini diimplementasikan menggunakan AuthorizationCodeFlow. Langkah-langkahnya adalah:

Atau, jika tidak menggunakan AuthorizationCodeFlow, Anda dapat menggunakan class tingkat lebih rendah:

Alur kode otorisasi Servlet

Library ini menyediakan class helper servlet untuk menyederhanakan alur kode otorisasi secara signifikan untuk kasus penggunaan dasar. Anda cukup memberikan subclass konkret AbstractAuthorizationCodeServlet dan AbstractAuthorizationCodeCallbackServlet (dari google-oauth-client-servlet) dan menambahkannya ke file web.xml. Perlu diperhatikan bahwa Anda masih harus mengurus login pengguna untuk aplikasi web Anda dan mengekstrak ID pengguna.

Contoh kode:

public class ServletSample extends AbstractAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

public class ServletCallbackSample extends AbstractAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

Alur kode otorisasi Google App Engine

Alur kode otorisasi di App Engine hampir identik dengan alur kode otorisasi servlet. Hanya saja, kita dapat memanfaatkan Users Java API dari Google App Engine. Pengguna harus login agar Users Java API dapat diaktifkan. Untuk informasi tentang cara mengalihkan pengguna ke halaman login jika mereka belum login, lihat Keamanan dan Autentikasi (di web.xml).

Perbedaan utama dengan kasus servlet adalah Anda menyediakan subclass konkret dari AbstractAppEngineAuthorizationCodeServlet dan AbstractAppEngineAuthorizationCodeCallbackServlet (dari google-oauth-client-appengine). Library ini memperluas class servlet abstrak dan menerapkan metode getUserId untuk Anda menggunakan Users Java API. AppEngineDataStoreFactory (dari Library Klien HTTP Google untuk Java adalah opsi yang baik untuk mempertahankan kredensial menggunakan Google App Engine Data Store API.

Contoh kode:

public class AppEngineSample extends AbstractAppEngineAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

public class AppEngineCallbackSample extends AbstractAppEngineAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

Alur kode otorisasi command line

Kode contoh sederhana yang diambil dari dailymotion-cmdline-sample:

/** Authorizes the installed application to access user's protected data. */
private static Credential authorize() throws Exception {
  OAuth2ClientCredentials.errorIfNotSpecified();
  // set up authorization code flow
  AuthorizationCodeFlow flow = new AuthorizationCodeFlow.Builder(BearerToken
      .authorizationHeaderAccessMethod(),
      HTTP_TRANSPORT,
      JSON_FACTORY,
      new GenericUrl(TOKEN_SERVER_URL),
      new ClientParametersAuthentication(
          OAuth2ClientCredentials.API_KEY, OAuth2ClientCredentials.API_SECRET),
      OAuth2ClientCredentials.API_KEY,
      AUTHORIZATION_SERVER_URL).setScopes(Arrays.asList(SCOPE))
      .setDataStoreFactory(DATA_STORE_FACTORY).build();
  // authorize
  LocalServerReceiver receiver = new LocalServerReceiver.Builder().setHost(
      OAuth2ClientCredentials.DOMAIN).setPort(OAuth2ClientCredentials.PORT).build();
  return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
}

private static void run(HttpRequestFactory requestFactory) throws IOException {
  DailyMotionUrl url = new DailyMotionUrl("https://api.dailymotion.com/videos/favorites");
  url.setFields("id,tags,title,url");

  HttpRequest request = requestFactory.buildGetRequest(url);
  VideoFeed videoFeed = request.execute().parseAs(VideoFeed.class);
  ...
}

public static void main(String[] args) {
  ...
  DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR);
  final Credential credential = authorize();
  HttpRequestFactory requestFactory =
      HTTP_TRANSPORT.createRequestFactory(new HttpRequestInitializer() {
        @Override
        public void initialize(HttpRequest request) throws IOException {
          credential.initialize(request);
          request.setParser(new JsonObjectParser(JSON_FACTORY));
        }
      });
  run(requestFactory);
  ...
}

Alur klien berbasis browser

Ini adalah langkah-langkah umum dalam alur klien berbasis browser yang ditentukan dalam spesifikasi Implisit Grant:

  • Dengan menggunakan BrowserClientRequestUrl, alihkan browser pengguna akhir ke halaman otorisasi tempat pengguna akhir dapat memberi aplikasi Anda akses ke data mereka yang dilindungi.
  • Gunakan aplikasi JavaScript untuk memproses token akses yang ditemukan di fragmen URL di URI pengalihan yang terdaftar dengan server otorisasi.

Contoh penggunaan untuk aplikasi web:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
  String url = new BrowserClientRequestUrl(
      "https://server.example.com/authorize", "s6BhdRkqt3").setState("xyz")
      .setRedirectUri("https://client.example.com/cb").build();
  response.sendRedirect(url);
}

Mendeteksi token akses yang sudah tidak berlaku

Menurut spesifikasi pemilik OAuth 2.0, saat server dipanggil untuk mengakses resource yang dilindungi dengan token akses yang sudah tidak berlaku, server biasanya merespons dengan kode status 401 Unauthorized HTTP seperti berikut:

   HTTP/1.1 401 Unauthorized
   WWW-Authenticate: Bearer realm="example",
                     error="invalid_token",
                     error_description="The access token expired"

Namun, tampaknya ada banyak fleksibilitas dalam spesifikasi. Untuk mengetahui detailnya, periksa dokumentasi penyedia OAuth 2.0.

Pendekatan alternatifnya adalah memeriksa parameter expires_in dalam respons token akses. Token ini menentukan masa aktif dalam hitungan detik dari token akses yang diberikan, yang biasanya satu jam. Namun, masa berlaku token akses mungkin belum habis pada akhir periode tersebut, dan server mungkin terus mengizinkan akses. Itulah sebabnya kami biasanya merekomendasikan untuk menunggu kode status 401 Unauthorized, daripada berasumsi bahwa masa berlaku token telah berakhir berdasarkan waktu yang telah berlalu. Atau, Anda dapat mencoba me-refresh token akses sesaat sebelum masa berlakunya habis, dan jika server token tidak tersedia, terus gunakan token akses tersebut sampai Anda menerima 401. Ini adalah strategi yang digunakan secara default di Credential.

Opsi lainnya adalah mengambil token akses baru sebelum setiap permintaan, tetapi hal ini memerlukan permintaan HTTP tambahan ke server token setiap saat, sehingga ini mungkin merupakan pilihan yang buruk dalam hal kecepatan dan penggunaan jaringan. Idealnya, simpan token akses di penyimpanan persisten yang aman untuk meminimalkan permintaan aplikasi untuk token akses baru. (Namun, untuk aplikasi yang terinstal, penyimpanan yang aman adalah masalah yang sulit.)

Perlu diperhatikan bahwa token akses dapat menjadi tidak valid karena alasan selain masa berlakunya habis, misalnya jika pengguna telah mencabut token secara eksplisit. Oleh karena itu, pastikan kode penanganan error Anda kuat. Setelah mendeteksi bahwa token tidak lagi valid, misalnya jika token sudah tidak berlaku atau dicabut, Anda harus menghapus token akses dari penyimpanan. Di Android, misalnya, Anda harus memanggil AccountManager.invalidateAuthToken.