Я знаю, что есть несколько постов, посвященных входу в систему/oauth для Android, но ни один из них не решает мою проблему.
Я изо всех сил пытаюсь согласовать поток аутентификации с GoogleSignInApi с использованием AccountManager в учебнике Google Sheets v4.
Используя GoogleSignInApi, я получаю код авторизации. Все идет нормально. Далее в документах рекомендуется обменять код авторизации на токен авторизации/восстановления. https://developers.google.com/identity/sign-in/android/offline-access есть отличный пример того, как отправить код авторизации в серверную часть для обмена.
Единственная проблема с этим потоком — у меня нет собственного бэкэнда, так как я просто хочу получить доступ к API-интерфейсу Google Sheets. Вызов API листов ожидает объект GoogleCredential, который я не могу получить из кода авторизации или иным образом через объект GoogleSignInAccount.
Итак, мои вопросы:
- Куда я могу отправить код авторизации, который я получил через GoogleSignInApi, чтобы обменять его на токен аутентификации.
- Есть ли библиотека, которая обрабатывает запрос на обмен и магию обновления, или я должен поймать токен обновления и сам выдать другой запрос токена аутентификации.
- Есть ли лучший способ получить правильные учетные данные для доступа к листам, а также использовать GoogleSignInApi для служб Firebase?
- Если я в конечном итоге использую GoogleAuthorizationCodeTokenRequest, как рекомендуется для доступа на стороне сервера, приемлемо ли использовать секрет клиента в клиенте? Возможно нет.
Вот упрощенная версия вызова API листов, который я пытаюсь сделать.
GoogleCredential credential = new GoogleCredential().setAccessToken("TEST_ACCESS_TOKEN_FROM_OAUTH_PLAYGROUND");
mService = new com.google.api.services.sheets.v4.Sheets.Builder(
transport, jsonFactory, credential)
.setApplicationName("Google Sheets API Android Quickstart")
.build();
ОБНОВЛЕНИЕ: чтобы добиться некоторого прогресса, я реализовал поток на стороне сервера для обмена токеном. Я почти уверен, что это неправильный метод, поскольку он требует использования client_secret в приложении.
Часть 1: SignInActivity основана на лаборатории кода Firebase. Мне нужна учетная запись firebase, поэтому я чувствую, что должен использовать GoogleSignInApi.
public class SignInActivity extends AppCompatActivity implements GoogleApiClient.OnConnectionFailedListener,
View.OnClickListener {
private static final int RC_SIGN_IN = 9001;
private GoogleApiClient mGoogleApiClient;
private FirebaseAuth mFirebaseAuth;
public static final String PREF_ACCOUNT_NAME = "accountName";
public static final String PREF_ID_TOKEN = "idToken";
public static final String PREF_AUTH_CODE = "authCode";
public static final Scope SHEETS_SCOPE = new Scope(SheetsScopes.SPREADSHEETS);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sign_in);
SignInButton signInButton = (SignInButton) findViewById(R.id.sign_in_button);
signInButton.setOnClickListener(this);
Log.d(TAG, getString(R.string.default_web_client_id));
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestScopes(SHEETS_SCOPE)
.requestServerAuthCode(getString(R.string.default_web_client_id))
.requestEmail()
.build();
mGoogleApiClient = new GoogleApiClient.Builder(this)
.enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */)
.addApi(Auth.GOOGLE_SIGN_IN_API, gso)
.build();
// Initialize FirebaseAuth
mFirebaseAuth = FirebaseAuth.getInstance();
}
private void handleFirebaseAuthResult(AuthResult authResult) {
// ...
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.sign_in_button:
signIn();
break;
default:
return;
}
}
private void signIn() {
Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
startActivityForResult(signInIntent, RC_SIGN_IN);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
if (requestCode == RC_SIGN_IN) {
GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
if (result.isSuccess()) {
// Google Sign In was successful, authenticate with Firebase
GoogleSignInAccount account = result.getSignInAccount();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext());
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PREF_ACCOUNT_NAME, account.getEmail());
editor.putString(PREF_ID_TOKEN, account.getIdToken());
editor.putString(PREF_AUTH_CODE, account.getServerAuthCode());
editor.apply();
// TODO: it would be great to do the exchange of the authcode now but it's doing a
// network call and can't be on the main thread.
// I really need this one
firebaseAuthWithGoogle(account);
} else {
// Google Sign In failed
Log.e(TAG, "Google Sign In failed.");
}
}
}
private void firebaseAuthWithGoogle(GoogleSignInAccount acct) {
Log.d(TAG, "firebaseAuthWithGoogle:" + acct.getId());
AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
mFirebaseAuth.signInWithCredential(credential)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful());
// If sign in fails, display a message to the user. If sign in succeeds
// the auth state listener will be notified and logic to handle the
// signed in user can be handled in the listener.
if (!task.isSuccessful()) {
Log.w(TAG, "signInWithCredential", task.getException());
Toast.makeText(SignInActivity.this, "Authentication failed.",
Toast.LENGTH_SHORT).show();
} else {
startActivity(new Intent(SignInActivity.this, MainActivity.class));
finish();
}
}
});
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
// An unresolvable error has occurred and Google APIs (including Sign-In) will not
// be available.
Log.d(TAG, "onConnectionFailed:" + connectionResult);
Toast.makeText(this, "Google Play Services error.", Toast.LENGTH_SHORT).show();
}
}
Часть 2. DataManager — это служебный класс, который используется приложением для доступа к данным листов. Он не использует поток, рекомендованный в лаборатории кода листов, поскольку он не позволяет мне настроить учетную запись firebase с теми же данными пользователя.
public class DataManager {
public static final String UNDEF = "undefined";
private com.google.api.services.sheets.v4.Sheets mService = null;
// this is the play copy
private static String mSheetID = SHEET_ID;
private static final String PREF_ACCESS_TOKEN = "accessToken";
private static final String PREF_REFRESH_TOKEN = "refreshToken";
private static final String PREF_EXPIRES_IN_SECONDS = "expiresInSec";
private Context mContext;
private String mAccessToken;
private String mRefreshToken;
private Long mExpiresInSeconds;
private String mAuthCode;
public DataManager(Context context) {
mContext = context;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
mAuthCode = prefs.getString(SignInActivity.PREF_AUTH_CODE, UNDEF);
mAccessToken = prefs.getString(PREF_ACCESS_TOKEN, UNDEF);
mRefreshToken = prefs.getString(PREF_REFRESH_TOKEN, UNDEF);
mExpiresInSeconds = prefs.getLong(PREF_EXPIRES_IN_SECONDS, 0);
}
private void exchangeCodeForToken(String authCode) {
try {
GoogleTokenResponse tokenResponse =
new GoogleAuthorizationCodeTokenRequest(
new NetHttpTransport(),
JacksonFactory.getDefaultInstance(),
"https://www.googleapis.com/oauth2/v4/token",
mContext.getString(R.string.default_web_client_id),
// TODO: the app shouldn't have to use the client secret
{CLIENT_SECRET},
authCode,
"")
.execute();
mAccessToken = tokenResponse.getAccessToken();
mRefreshToken = tokenResponse.getRefreshToken();
mExpiresInSeconds = tokenResponse.getExpiresInSeconds();
// TODO: do I really need to store and pass the three values individually?
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PREF_ACCESS_TOKEN, mAccessToken);
editor.putString(PREF_REFRESH_TOKEN, mRefreshToken);
editor.putLong(PREF_EXPIRES_IN_SECONDS, mExpiresInSeconds);
editor.remove(SignInActivity.PREF_AUTH_CODE);
editor.apply();
} catch (Exception e) {
Log.e(TAG, "Token exchange failed with " + e.getMessage());
}
}
private void refreshAccessToken(String refreshToken) {
try {
// TODO: what to do here?
throw new Exception("TBD");
} catch (Exception e) {
Log.e(TAG, "Token refresh failed with " + e.getMessage());
}
}
private GoogleCredential getCredential() {
if (mAuthCode != UNDEF) {
exchangeCodeForToken(mAuthCode);
}
// TODO: handle missing or expired token
if (mRefreshToken != UNDEF && mExpiresInSeconds < 30) {
refreshAccessToken(mRefreshToken);
}
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(new NetHttpTransport())
.setJsonFactory(JacksonFactory.getDefaultInstance())
.build();
credential.setAccessToken(mAccessToken);
if (mRefreshToken != UNDEF) {
credential.setRefreshToken(mRefreshToken);
credential.setExpiresInSeconds(mExpiresInSeconds);
}
return credential;
}
// Set up credential and service object, then issue api call.
public ArrayList<Foo> getFooListFromServer() throws IOException {
try {
GoogleCredential credential = getCredential();
HttpTransport transport = AndroidHttp.newCompatibleTransport();
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
mService = new com.google.api.services.sheets.v4.Sheets.Builder(
transport, jsonFactory, credential)
.setApplicationName(mContext.getString(R.string.app_name))
.build();
return getDataFromServer();
} catch (IOException exception) {
// ...
throw exception;
} catch (Exception e) {
Log.e(TAG, "something else is going on " + e.toString());
throw e;
}
}
/**
* Actually fetch the data from google
*
* @return List of Foos
* @throws IOException
*/
private ArrayList<Foo> getDataFromServer() throws IOException {
ArrayList<Foo> foos = new ArrayList<Foo>();
ValueRange response = this.mService.spreadsheets().values()
.get(mSheetID, mRange)
.setValueRenderOption("UNFORMATTED_VALUE")
.setDateTimeRenderOption("FORMATTED_STRING")
.execute();
//...
return foos;
}
}