OAuth 2.0 e libreria client OAuth di Google per Java

Panoramica

Scopo: questo documento descrive le funzioni OAuth 2.0 generiche offerte dalla libreria client OAuth di Google per Java. Puoi utilizzare queste funzioni per l'autenticazione e l'autorizzazione per qualsiasi servizio internet.

Per istruzioni su come utilizzare GoogleCredential per eseguire l'autorizzazione OAuth 2.0 con i servizi Google, consulta Utilizzare OAuth 2.0 con la libreria client dell'API Google per Java.

Riepilogo: OAuth 2.0 è una specifica standard che consente agli utenti finali di autorizzare in sicurezza un'applicazione client ad accedere a risorse lato server protette. Inoltre, la specifica del token bearer OAuth 2.0 spiega come accedere a queste risorse protette utilizzando un token di accesso concesso durante la procedura di autorizzazione dell'utente finale.

Per maggiori dettagli, consulta la documentazione Javadoc per i seguenti pacchetti:

Registrazione del cliente

Prima di utilizzare la libreria client OAuth di Google per Java, probabilmente dovrai registrare la tua applicazione con un server di autorizzazione per ricevere un ID client e un client secret. Per informazioni generali su questa procedura, consulta la specifica relativa alla registrazione client.

Credenziali e archivio delle credenziali

Credential è una classe di assistenza OAuth 2.0 sicura per i thread per accedere alle risorse protette utilizzando un token di accesso. Se utilizzi un token di aggiornamento, Credential aggiorna anche il token di accesso quando scade utilizzando il token di aggiornamento. Ad esempio, se hai già un token di accesso, puoi effettuare una richiesta nel seguente modo:

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

La maggior parte delle applicazioni deve mantenere il token di accesso e il token di aggiornamento della credenziale per evitare un futuro reindirizzamento alla pagina di autorizzazione nel browser. L'implementazione di CredentialStore in questa libreria è deprecata e verrà rimossa nelle release future. L'alternativa è utilizzare le interfacce DataStoreFactory e DataStore con StoredCredential, fornite dalla libreria client HTTP di Google per Java.

Puoi utilizzare una delle seguenti implementazioni fornite dalla libreria:

Utenti di Google App Engine:

AppEngineCredentialStore è deprecato e verrà rimosso.

Ti consigliamo di utilizzare AppEngineDataStoreFactory con StoredCredential. Se hai credenziali archiviate nel vecchio modo, puoi utilizzare i metodi di assistenza aggiunti migrateTo(AppEngineDataStoreFactory) o migrateTo(DataStore) per eseguire la migrazione.

Utilizza DataStoreCredentialRefreshListener e impostalo per la credenziale utilizzando GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener).

Flusso del codice di autorizzazione

Utilizza il flusso del codice di autorizzazione per consentire all'utente finale di concedere alla tua applicazione accesso ai suoi dati protetti. Il protocollo per questo flusso è specificato nella specifica della concessione del codice di autorizzazione.

Questo flusso viene implementato utilizzando AuthorizationCodeFlow. I passaggi sono:

In alternativa, se non utilizzi AuthorizationCodeFlow, puoi utilizzare le classi di livello inferiore:

Flusso del codice di autorizzazione del servlet

Questa libreria fornisce classi helper servlet per semplificare notevolmente il flusso del codice di autorizzazione per i casi d'uso di base. Devi solo fornire sottoclassi concrete di AbstractAuthorizationCodeServlet e AbstractAuthorizationCodeCallbackServlet (da google-oauth-client-servlet) e aggiungerle al file web.xml. Tieni presente che devi comunque occuparti dell'accesso degli utenti alla tua applicazione web ed estrarre un ID utente.

Codice di esempio:

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
  }
}

Flusso del codice di autorizzazione di Google App Engine

Il flusso del codice di autorizzazione su App Engine è quasi identico al flusso del codice di autorizzazione servlet, ma possiamo sfruttare l'API Users Java di Google App Engine. L'utente deve aver eseguito l'accesso affinché l'API Java Users venga attivata. Per informazioni su come reindirizzare gli utenti a una pagina di accesso se non hanno ancora eseguito l'accesso, consulta Sicurezza e autenticazione (in web.xml).

La differenza principale rispetto al caso del servlet è che fornisci sottoclassi concrete di AbstractAppEngineAuthorizationCodeServlet e AbstractAppEngineAuthorizationCodeCallbackServlet (da google-oauth-client-appengine). Estendono le classi servlet astratte e implementano il metodo getUserId per te utilizzando l'API Java Users. AppEngineDataStoreFactory (della libreria client HTTP di Google per Java) è una buona opzione per la persistenza della credenziale utilizzando l'API Google App Engine Data Store.

Codice di esempio:

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

Flusso del codice di autorizzazione dalla riga di comando

Codice di esempio semplificato tratto da 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);
  ...
}

Flusso di clienti basato su browser

Di seguito sono riportati i passaggi tipici del flusso del client basato su browser specificato nella specifica della concessione implicita:

  • Utilizzando BrowserClientRequestUrl, reindirizza il browser dell'utente finale alla pagina di autorizzazione in cui l'utente finale può concedere alla tua applicazione l'accesso ai suoi dati protetti.
  • Utilizza un'applicazione JavaScript per elaborare il token di accesso trovato nel frammento dell'URL nell'URI di reindirizzamento registrato con il server di autorizzazione.

Esempio di utilizzo per un'applicazione 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);
}

Rilevamento di un token di accesso scaduto

In base alla specifica del token bearer OAuth 2.0, quando il server viene chiamato per accedere a una risorsa protetta con un token di accesso scadeto, tipicamente risponde con un codice di stato HTTP 401 Unauthorized, come il seguente:

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

Tuttavia, sembra esserci molta flessibilità nella specifica. Per maggiori dettagli, consulta la documentazione del provider OAuth 2.0.

Un approccio alternativo è controllare il parametro expires_in nella risposta del token di accesso. Specifica la durata in secondi del token di accesso concesso, che in genere è di un'ora. Tuttavia, il token di accesso potrebbe non scadere effettivamente alla fine di questo periodo e il server potrebbe continuare a consentire l'accesso. Per questo motivo, solitamente consigliamo di attendere un codice di stato 401 Unauthorized anziché presumere che il token sia scaduto in base al tempo trascorso. In alternativa, puoi provare ad aggiornare un token di accesso poco prima della scadenza e, se il server dei token non è disponibile, continuare a utilizzare il token di accesso finché non ricevi un 401. Questa è la strategia utilizzata per impostazione predefinita in Credential.

Un'altra opzione è quella di recuperare un nuovo token di accesso prima di ogni richiesta, ma ciò richiede ogni volta una richiesta HTTP aggiuntiva al server token, quindi è probabilmente una scelta scarsa in termini di velocità e utilizzo della rete. Idealmente, memorizza il token di accesso in uno spazio di archiviazione sicuro e permanente per ridurre al minimo le richieste di nuovi token di accesso da parte di un'applicazione. Tuttavia, per le applicazioni installate, lo spazio di archiviazione sicuro è un problema difficile.

Tieni presente che un token di accesso potrebbe non essere valido per motivi diversi dalla scadenza, ad esempio se l'utente ha revocato esplicitamente il token, quindi assicurati che il codice di gestione degli errori sia affidabile. Una volta rilevato che un token non è più valido, ad esempio se è scaduto o è stato revocato, devi rimuovere il token di accesso dallo spazio di archiviazione. Ad esempio, su Android devi chiamare AccountManager.invalidateAuthToken.