增加google第三方登录相关代码

This commit is contained in:
cebgcontract 2022-09-13 11:44:09 +08:00
parent 8db5491391
commit 5fd6c4a682
10 changed files with 1030 additions and 82 deletions

17
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_4_API_28_2.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-09-09T05:20:37.906784Z" />
</component>
</project>

1
.idea/gradle.xml generated
View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.fitchgc.headlesscocos">
<application
@ -14,6 +15,7 @@
android:value="cocos2djs" />
<activity android:name=".MainActivity"
android:screenOrientation="sensorLandscape"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -26,6 +28,32 @@
<activity
android:name="com.king.zxing.CaptureActivity"
android:theme="@style/CaptureTheme"/>
<!--
This activity declaration is merged with the version from the library manifest.
It demonstrates how an https redirect can be captured, in addition to or instead of
a custom scheme.
Generally, this should be done in conjunction with an app link declaration for Android M
and above, for additional security and an improved user experience. See:
https://developer.android.com/training/app-links/index.html
The declaration from the library can be completely replaced by adding
tools:node="replace"
To the list of attributes on the activity element.
-->
<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
tools:node="replace">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="com.googleusercontent.apps.165555585193-ud80sst45po348ohec2h33t2m6mjnlt0"/>
</intent-filter>
</activity>
</application>
<supports-screens
android:anyDensity="true"

View File

@ -39,6 +39,10 @@ android {
}
}
}
manifestPlaceholders = [
'appAuthRedirectScheme': 'com.googleusercontent.apps.165555585193-ud80sst45po348ohec2h33t2m6mjnlt0'
]
}
sourceSets.main {
@ -57,10 +61,28 @@ android {
}
}
signingConfigs {
debug {
storeFile file("${rootDir}/keys/release")
storePassword '7654321'
keyAlias 'release'
keyPassword '7654321'
}
release {
storeFile file("${rootDir}/keys/release")
storePassword "7654321"
keyAlias "release"
keyPassword "7654321"
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
minifyEnabled false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
@ -114,4 +136,6 @@ dependencies {
implementation 'pub.devrel:easypermissions:3.0.0'
implementation 'com.github.jenly1314:zxing-lite:2.1.1'
implementation project(path: ':libcocos2dx')
implementation 'net.openid:appauth:0.11.1'
implementation "com.squareup.okio:okio:2.10.0"
}

View File

@ -4,40 +4,68 @@ import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Window;
import com.google.android.gms.auth.api.identity.BeginSignInRequest;
import com.google.android.gms.auth.api.identity.BeginSignInResult;
import com.google.android.gms.auth.api.identity.Identity;
import com.google.android.gms.auth.api.identity.SignInClient;
import com.google.android.gms.auth.api.identity.SignInCredential;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.common.api.Scope;
import com.google.android.gms.tasks.Task;
import com.jc.jcfw.JcSDK;
import com.jc.jcfw.appauth.AuthStateManager;
import com.jc.jcfw.appauth.JConfiguration;
import com.king.zxing.CameraScan;
import com.king.zxing.CaptureActivity;
import com.king.zxing.util.CodeUtils;
import com.king.zxing.util.LogUtils;
import com.unity3d.player.UnityPlayer;
import net.openid.appauth.AppAuthConfiguration;
import net.openid.appauth.AuthState;
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.AuthorizationRequest;
import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.AuthorizationService;
import net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.ClientAuthentication;
import net.openid.appauth.ClientSecretBasic;
import net.openid.appauth.RegistrationRequest;
import net.openid.appauth.RegistrationResponse;
import net.openid.appauth.ResponseTypeValues;
import net.openid.appauth.TokenRequest;
import net.openid.appauth.TokenResponse;
import net.openid.appauth.browser.AnyBrowserMatcher;
import net.openid.appauth.browser.BrowserMatcher;
import org.cocos2dx.lib.Cocos2dxHelper;
import org.cocos2dx.lib.CocosJSHelper;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.app.ActivityOptionsCompat;
import pub.devrel.easypermissions.AfterPermissionGranted;
import pub.devrel.easypermissions.EasyPermissions;
@ -54,9 +82,13 @@ public class MainActivity extends Activity
public static final String KEY_IS_QR_CODE = "key_code";
public static final String KEY_IS_CONTINUOUS = "key_continuous_scan";
// scan QRCode
public static final int REQUEST_CODE_SCAN = 0X01;
public static final int REQUEST_CODE_PHOTO = 0X02;
public static final int REQ_ONE_TAP = 0X03;
// AppAuth
private static final int RC_AUTH = 0X04;
// google signin
private static final int RC_SIGN_IN = 0X05;
public static final int RC_CAMERA = 0X01;
@ -64,8 +96,22 @@ public class MainActivity extends Activity
private String title;
private String funId;
private SignInClient oneTapClient;
private BeginSignInRequest signInRequest;
//AppAuth
private AuthorizationService mAuthService;
private AuthStateManager mAuthStateManager;
private JConfiguration mConfiguration;
private ExecutorService mExecutor;
private final AtomicReference<String> mClientId = new AtomicReference<>();
private final AtomicReference<AuthorizationRequest> mAuthRequest = new AtomicReference<>();
private final AtomicReference<CustomTabsIntent> mAuthIntent = new AtomicReference<>();
private CountDownLatch mAuthIntentLatch = new CountDownLatch(1);
private static final String EXTRA_FAILED = "failed";
@NonNull
private final BrowserMatcher mBrowserMatcher = AnyBrowserMatcher.INSTANCE;
private GoogleSignInClient mGoogleSignInClient;
protected String updateUnityCommandLineArguments(String cmdLine) {
return cmdLine;
@ -89,21 +135,19 @@ public class MainActivity extends Activity
Cocos2dxHelper.init(this);
CocosJSHelper.initJSEnv(getApplicationContext());
oneTapClient = Identity.getSignInClient(this);
signInRequest = BeginSignInRequest.builder()
.setPasswordRequestOptions(BeginSignInRequest.PasswordRequestOptions.builder()
.setSupported(true)
.build())
.setGoogleIdTokenRequestOptions(BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
// Your server's client ID, not your Android client ID.
.setServerClientId(getString(R.string.default_web_client_id))
// Only show accounts previously used to sign in.
.setFilterByAuthorizedAccounts(true)
.build())
// Automatically sign in when exactly one credential is retrieved.
.setAutoSelectEnabled(true)
.build();
// begin of google sign
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestScopes(new Scope("https://www.googleapis.com/auth/drive.appdata"))
.build();
mGoogleSignInClient = GoogleSignIn.getClient(this, gso);
// end of google sign
mExecutor = Executors.newSingleThreadExecutor();
mAuthStateManager = AuthStateManager.getInstance(this);
mConfiguration = JConfiguration.getInstance(this);
mExecutor.submit(this::initializeAppAuth);
}
@Override
@ -121,38 +165,26 @@ public class MainActivity extends Activity
case REQUEST_CODE_PHOTO:
parsePhoto(data);
break;
case REQ_ONE_TAP:
try {
SignInCredential credential = oneTapClient.getSignInCredentialFromIntent(data);
String idToken = credential.getGoogleIdToken();
String username = credential.getId();
String password = credential.getPassword();
if (idToken != null) {
// Got an ID token from Google. Use it to authenticate
// with your backend.
Log.d(TAG, "Got ID token: " + idToken);
} else if (password != null) {
// Got a saved username and password. Use them to authenticate
// with your backend.
Log.d(TAG, "Got password.");
}
} catch (ApiException e) {
switch (e.getStatusCode()) {
case CommonStatusCodes.CANCELED:
Log.d(TAG, "One-tap dialog was closed.");
// Don't re-prompt the user.
// showOneTapUI = false;
break;
case CommonStatusCodes.NETWORK_ERROR:
Log.d(TAG, "One-tap encountered a network error.");
// Try again or just ignore.
break;
default:
Log.d(TAG, "Couldn't get credential from result."
+ e.getLocalizedMessage());
break;
}
case RC_AUTH:
AuthorizationResponse response = AuthorizationResponse.fromIntent(data);
AuthorizationException ex = AuthorizationException.fromIntent(data);
if (response != null || ex != null) {
mAuthStateManager.updateAfterAuthorization(response, ex);
}
if (response != null && response.authorizationCode != null) {
// authorization code exchange is required
mAuthStateManager.updateAfterAuthorization(response, ex);
exchangeAuthorizationCode(response);
} else if (ex != null) {
Log.i(TAG, "Authorization flow failed: " + ex.getMessage());
} else {
Log.i(TAG, "No authorization state retained - reauthorization required");
}
break;
case RC_SIGN_IN:
Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
handleSignInResult(task);
break;
}
} else {
@ -169,6 +201,10 @@ public class MainActivity extends Activity
protected void onStart() {
super.onStart();
mUnityPlayer.start();
if (mExecutor.isShutdown()) {
mExecutor = Executors.newSingleThreadExecutor();
}
}
// Resume Unity
@ -375,32 +411,294 @@ public class MainActivity extends Activity
// end of qrcode
public void signWithGoogle(String funId) {
oneTapClient.beginSignIn(signInRequest)
.addOnSuccessListener(this, new OnSuccessListener<BeginSignInResult>() {
@Override
public void onSuccess(BeginSignInResult result) {
try {
startIntentSenderForResult(
result.getPendingIntent().getIntentSender(), REQ_ONE_TAP,
null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
Log.e(TAG, "Couldn't start One Tap UI: " + e.getLocalizedMessage());
}
}
})
.addOnFailureListener(this, new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
// No saved credentials found. Launch the One Tap sign-up flow, or
// do nothing and continue presenting the signed-out UI.
Log.d(TAG, e.getLocalizedMessage());
}
});
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) {
GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
if (account != null) {
Log.w(TAG, "already login: " + account.getIdToken());
} else {
Intent signInIntent = mGoogleSignInClient.getSignInIntent();
startActivityForResult(signInIntent, RC_SIGN_IN);
}
} else {
Log.i(TAG, "no gms, use app auth");
AuthState state = mAuthStateManager.getCurrent();
if (state.isAuthorized() && state.getIdToken() != null) {
Log.w(TAG, "already login: " + state.getIdToken());
} else {
mExecutor.submit(this::doAuth);
}
}
}
public void signOutGoogle(String funId) {
oneTapClient.signOut();
mGoogleSignInClient.signOut()
.addOnCompleteListener(this, task -> {
// ...
});
}
private void handleSignInResult(Task<GoogleSignInAccount> completedTask) {
try {
GoogleSignInAccount account = completedTask.getResult(ApiException.class);
Log.w(TAG, "signIn success: ");
Log.w(TAG, "idToken: " + account.getIdToken());
// Signed in successfully, show authenticated UI.
} catch (ApiException e) {
// The ApiException status code indicates the detailed failure reason.
// Please refer to the GoogleSignInStatusCodes class reference for more information.
Log.w(TAG, "signInResult:failed code=" + e.getStatusCode());
}
}
// begin of AppAuth
/**
* Initializes the authorization service configuration if necessary, either from the local
* static values or by retrieving an OpenID discovery document.
*/
@WorkerThread
private void initializeAppAuth() {
Log.i(TAG, "Initializing AppAuth");
recreateAuthorizationService();
if (mAuthStateManager.getCurrent().getAuthorizationServiceConfiguration() != null) {
// configuration is already created, skip to client initialization
Log.i(TAG, "auth config already established");
initializeClient();
return;
}
// if we are not using discovery, build the authorization service configuration directly
// from the static configuration values.
if (mConfiguration.getDiscoveryUri() == null) {
Log.i(TAG, "Creating auth config from res/raw/auth_config.json");
AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(
mConfiguration.getAuthEndpointUri(),
mConfiguration.getTokenEndpointUri(),
mConfiguration.getRegistrationEndpointUri(),
mConfiguration.getEndSessionEndpoint());
mAuthStateManager.replace(new AuthState(config));
initializeClient();
return;
}
// WrongThread inference is incorrect for lambdas
// noinspection WrongThread
Log.i(TAG, "Retrieving OpenID discovery doc");
AuthorizationServiceConfiguration.fetchFromUrl(
mConfiguration.getDiscoveryUri(),
this::handleConfigurationRetrievalResult,
mConfiguration.getConnectionBuilder());
}
@MainThread
private void handleConfigurationRetrievalResult(
AuthorizationServiceConfiguration config,
AuthorizationException ex) {
if (config == null) {
Log.i(TAG, "Failed to retrieve discovery document", ex);
return;
}
Log.i(TAG, "Discovery document retrieved");
mAuthStateManager.replace(new AuthState(config));
mExecutor.submit(this::initializeClient);
}
private void recreateAuthorizationService() {
if (mAuthService != null) {
Log.i(TAG, "Discarding existing AuthService instance");
mAuthService.dispose();
}
mAuthService = createAuthorizationService();
mAuthRequest.set(null);
mAuthIntent.set(null);
}
private AuthorizationService createAuthorizationService() {
Log.i(TAG, "Creating authorization service");
AppAuthConfiguration.Builder builder = new AppAuthConfiguration.Builder();
builder.setBrowserMatcher(mBrowserMatcher);
builder.setConnectionBuilder(mConfiguration.getConnectionBuilder());
return new AuthorizationService(this, builder.build());
}
private void createAuthRequest(@Nullable String loginHint) {
Log.i(TAG, "Creating auth request for login hint: " + loginHint);
AuthorizationRequest.Builder authRequestBuilder = new AuthorizationRequest.Builder(
mAuthStateManager.getCurrent().getAuthorizationServiceConfiguration(),
mClientId.get(),
ResponseTypeValues.CODE,
mConfiguration.getRedirectUri())
.setScope(mConfiguration.getScope());
if (!TextUtils.isEmpty(loginHint)) {
authRequestBuilder.setLoginHint(loginHint);
}
mAuthRequest.set(authRequestBuilder.build());
}
private void warmUpBrowser() {
mAuthIntentLatch = new CountDownLatch(1);
mExecutor.execute(() -> {
Log.i(TAG, "Warming up browser instance for auth request");
CustomTabsIntent.Builder intentBuilder =
mAuthService.createCustomTabsIntentBuilder(mAuthRequest.get().toUri());
mAuthIntent.set(intentBuilder.build());
mAuthIntentLatch.countDown();
});
}
@MainThread
private void initializeAuthRequest() {
createAuthRequest("");
warmUpBrowser();
displayAuthOptions();
}
/**
* Initiates a dynamic registration request if a client ID is not provided by the static
* configuration.
*/
@WorkerThread
private void initializeClient() {
if (mConfiguration.getClientId() != null) {
Log.i(TAG, "Using static client ID: " + mConfiguration.getClientId());
// use a statically configured client ID
mClientId.set(mConfiguration.getClientId());
runOnUiThread(this::initializeAuthRequest);
return;
}
RegistrationResponse lastResponse =
mAuthStateManager.getCurrent().getLastRegistrationResponse();
if (lastResponse != null) {
Log.i(TAG, "Using dynamic client ID: " + lastResponse.clientId);
// already dynamically registered a client ID
mClientId.set(lastResponse.clientId);
runOnUiThread(this::initializeAuthRequest);
return;
}
// WrongThread inference is incorrect for lambdas
// noinspection WrongThread
Log.i(TAG, "Dynamically registering client");
RegistrationRequest registrationRequest = new RegistrationRequest.Builder(
mAuthStateManager.getCurrent().getAuthorizationServiceConfiguration(),
Collections.singletonList(mConfiguration.getRedirectUri()))
.setTokenEndpointAuthenticationMethod(ClientSecretBasic.NAME)
.build();
mAuthService.performRegistrationRequest(
registrationRequest,
this::handleRegistrationResponse);
}
private void handleRegistrationResponse(
RegistrationResponse response,
AuthorizationException ex) {
mAuthStateManager.updateAfterRegistration(response, ex);
if (response == null) {
Log.i(TAG, "Failed to dynamically register client", ex);
return;
}
Log.i(TAG, "Dynamically registered client: " + response.clientId);
mClientId.set(response.clientId);
initializeAuthRequest();
}
private void displayAuthOptions() {
AuthState state = mAuthStateManager.getCurrent();
AuthorizationServiceConfiguration config = state.getAuthorizationServiceConfiguration();
String authEndpointStr;
if (config.discoveryDoc != null) {
authEndpointStr = "Discovered auth endpoint: \n";
} else {
authEndpointStr = "Static auth endpoint: \n";
}
authEndpointStr += config.authorizationEndpoint;
Log.i(TAG, authEndpointStr);
String clientIdStr;
if (state.getLastRegistrationResponse() != null) {
clientIdStr = "Dynamic client ID: \n";
} else {
clientIdStr = "Static client ID: \n";
}
clientIdStr += mClientId;
Log.i(TAG, clientIdStr);
}
/**
* Performs the authorization request, using the browser selected in the spinner,
* and a user-provided `login_hint` if available.
*/
@WorkerThread
private void doAuth() {
try {
mAuthIntentLatch.await();
} catch (InterruptedException ex) {
Log.w(TAG, "Interrupted while waiting for auth intent");
}
Intent intent = mAuthService.getAuthorizationRequestIntent(
mAuthRequest.get(),
mAuthIntent.get());
startActivityForResult(intent, RC_AUTH);
}
@MainThread
private void exchangeAuthorizationCode(AuthorizationResponse authorizationResponse) {
Log.d(TAG, "Exchanging authorization code");
performTokenRequest(
authorizationResponse.createTokenExchangeRequest(),
this::handleCodeExchangeResponse);
}
@MainThread
private void performTokenRequest(
TokenRequest request,
AuthorizationService.TokenResponseCallback callback) {
ClientAuthentication clientAuthentication;
try {
clientAuthentication = mAuthStateManager.getCurrent().getClientAuthentication();
} catch (ClientAuthentication.UnsupportedAuthenticationMethod ex) {
Log.d(TAG, "Token request cannot be made, client authentication for the token "
+ "endpoint could not be constructed (%s)", ex);
return;
}
mAuthService.performTokenRequest(
request,
clientAuthentication,
callback);
}
@WorkerThread
private void handleCodeExchangeResponse(
@Nullable TokenResponse tokenResponse,
@Nullable AuthorizationException authException) {
mAuthStateManager.updateAfterTokenResponse(tokenResponse, authException);
if (!mAuthStateManager.getCurrent().isAuthorized()) {
final String message = "Authorization Code exchange failed"
+ ((authException != null) ? authException.error : "");
// WrongThread inference is incorrect for lambdas
//noinspection WrongThread
Log.d(TAG, message);
} else {
Log.d(TAG, "success");
AuthState state = mAuthStateManager.getCurrent();
Log.d(TAG, String.valueOf(state.isAuthorized()));
Log.d(TAG, String.valueOf(state.getIdToken()));
mAuthStateManager.replace(state);
}
}
}

View File

@ -0,0 +1,150 @@
package com.jc.jcfw.appauth;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import net.openid.appauth.AuthState;
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.RegistrationResponse;
import net.openid.appauth.TokenResponse;
import org.json.JSONException;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class AuthStateManager {
private static final AtomicReference<WeakReference<AuthStateManager>> INSTANCE_REF =
new AtomicReference<>(new WeakReference<>(null));
private static final String TAG = "AuthStateManager";
private static final String STORE_NAME = "AuthState";
private static final String KEY_STATE = "state";
private final SharedPreferences mPrefs;
private final ReentrantLock mPrefsLock;
private final AtomicReference<AuthState> mCurrentAuthState;
@AnyThread
public static AuthStateManager getInstance(@NonNull Context context) {
AuthStateManager manager = INSTANCE_REF.get().get();
if (manager == null) {
manager = new AuthStateManager(context.getApplicationContext());
INSTANCE_REF.set(new WeakReference<>(manager));
}
return manager;
}
private AuthStateManager(Context context) {
mPrefs = context.getSharedPreferences(STORE_NAME, Context.MODE_PRIVATE);
mPrefsLock = new ReentrantLock();
mCurrentAuthState = new AtomicReference<>();
}
@AnyThread
@NonNull
public AuthState getCurrent() {
if (mCurrentAuthState.get() != null) {
return mCurrentAuthState.get();
}
AuthState state = readState();
if (mCurrentAuthState.compareAndSet(null, state)) {
return state;
} else {
return mCurrentAuthState.get();
}
}
@AnyThread
@NonNull
public AuthState replace(@NonNull AuthState state) {
writeState(state);
mCurrentAuthState.set(state);
return state;
}
@AnyThread
@NonNull
public AuthState updateAfterAuthorization(
@Nullable AuthorizationResponse response,
@Nullable AuthorizationException ex) {
AuthState current = getCurrent();
current.update(response, ex);
return replace(current);
}
@AnyThread
@NonNull
public AuthState updateAfterTokenResponse(
@Nullable TokenResponse response,
@Nullable AuthorizationException ex) {
AuthState current = getCurrent();
current.update(response, ex);
return replace(current);
}
@AnyThread
@NonNull
public AuthState updateAfterRegistration(
RegistrationResponse response,
AuthorizationException ex) {
AuthState current = getCurrent();
if (ex != null) {
return current;
}
current.update(response);
return replace(current);
}
@AnyThread
@NonNull
private AuthState readState() {
mPrefsLock.lock();
try {
String currentState = mPrefs.getString(KEY_STATE, null);
if (currentState == null) {
return new AuthState();
}
try {
return AuthState.jsonDeserialize(currentState);
} catch (JSONException ex) {
Log.w(TAG, "Failed to deserialize stored auth state - discarding");
return new AuthState();
}
} finally {
mPrefsLock.unlock();
}
}
@AnyThread
private void writeState(@Nullable AuthState state) {
mPrefsLock.lock();
try {
SharedPreferences.Editor editor = mPrefs.edit();
if (state == null) {
editor.remove(KEY_STATE);
} else {
editor.putString(KEY_STATE, state.jsonSerializeString());
}
if (!editor.commit()) {
throw new IllegalStateException("Failed to write state to shared prefs");
}
} finally {
mPrefsLock.unlock();
}
}
}

View File

@ -0,0 +1,108 @@
package com.jc.jcfw.appauth;
import android.annotation.SuppressLint;
import android.net.Uri;
import android.util.Log;
import net.openid.appauth.Preconditions;
import net.openid.appauth.connectivity.ConnectionBuilder;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public final class ConnectionBuilderForTesting implements ConnectionBuilder {
public static final ConnectionBuilderForTesting INSTANCE = new ConnectionBuilderForTesting();
private static final String TAG = "ConnBuilder";
private static final int CONNECTION_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(15);
private static final int READ_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10);
private static final String HTTP = "http";
private static final String HTTPS = "https";
@SuppressLint("TrustAllX509TrustManager")
private static final TrustManager[] ANY_CERT_MANAGER = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}
};
@SuppressLint("BadHostnameVerifier")
private static final HostnameVerifier ANY_HOSTNAME_VERIFIER = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
@Nullable
private static final SSLContext TRUSTING_CONTEXT;
static {
SSLContext context;
try {
context = SSLContext.getInstance("SSL");
} catch (NoSuchAlgorithmException e) {
Log.e("ConnBuilder", "Unable to acquire SSL context");
context = null;
}
SSLContext initializedContext = null;
if (context != null) {
try {
context.init(null, ANY_CERT_MANAGER, new java.security.SecureRandom());
initializedContext = context;
} catch (KeyManagementException e) {
Log.e(TAG, "Failed to initialize trusting SSL context");
}
}
TRUSTING_CONTEXT = initializedContext;
}
private ConnectionBuilderForTesting() {
// no need to construct new instances
}
@NonNull
@Override
public HttpURLConnection openConnection(@NonNull Uri uri) throws IOException {
Preconditions.checkNotNull(uri, "url must not be null");
Preconditions.checkArgument(HTTP.equals(uri.getScheme()) || HTTPS.equals(uri.getScheme()),
"scheme or uri must be http or https");
HttpURLConnection conn = (HttpURLConnection) new URL(uri.toString()).openConnection();
conn.setConnectTimeout(CONNECTION_TIMEOUT_MS);
conn.setReadTimeout(READ_TIMEOUT_MS);
conn.setInstanceFollowRedirects(false);
if (conn instanceof HttpsURLConnection && TRUSTING_CONTEXT != null) {
HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
httpsConn.setSSLSocketFactory(TRUSTING_CONTEXT.getSocketFactory());
httpsConn.setHostnameVerifier(ANY_HOSTNAME_VERIFIER);
}
return conn;
}
}

View File

@ -0,0 +1,310 @@
package com.jc.jcfw.appauth;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.text.TextUtils;
import com.fitchgc.headlesscocos.R;
import net.openid.appauth.connectivity.ConnectionBuilder;
import net.openid.appauth.connectivity.DefaultConnectionBuilder;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.charset.Charset;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import okio.Buffer;
import okio.BufferedSource;
import okio.Okio;
public final class JConfiguration {
private static final String TAG = "Configuration";
private static final String PREFS_NAME = "config";
private static final String KEY_LAST_HASH = "lastHash";
private static WeakReference<JConfiguration> sInstance = new WeakReference<>(null);
private final Context mContext;
private final SharedPreferences mPrefs;
private final Resources mResources;
private JSONObject mConfigJson;
private String mConfigHash;
private String mConfigError;
private String mClientId;
private String mScope;
private Uri mRedirectUri;
private Uri mEndSessionRedirectUri;
private Uri mDiscoveryUri;
private Uri mAuthEndpointUri;
private Uri mTokenEndpointUri;
private Uri mEndSessionEndpoint;
private Uri mRegistrationEndpointUri;
private Uri mUserInfoEndpointUri;
private boolean mHttpsRequired;
public static JConfiguration getInstance(Context context) {
JConfiguration config = sInstance.get();
if (config == null) {
config = new JConfiguration(context);
sInstance = new WeakReference<JConfiguration>(config);
}
return config;
}
public JConfiguration(Context context) {
mContext = context;
mPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
mResources = context.getResources();
try {
readConfiguration();
} catch (InvalidConfigurationException ex) {
mConfigError = ex.getMessage();
}
}
/**
* Indicates whether the configuration has changed from the last known valid state.
*/
public boolean hasConfigurationChanged() {
String lastHash = getLastKnownConfigHash();
return !mConfigHash.equals(lastHash);
}
/**
* Indicates whether the current configuration is valid.
*/
public boolean isValid() {
return mConfigError == null;
}
/**
* Returns a description of the configuration error, if the configuration is invalid.
*/
@Nullable
public String getConfigurationError() {
return mConfigError;
}
/**
* Indicates that the current configuration should be accepted as the "last known valid"
* configuration.
*/
public void acceptConfiguration() {
mPrefs.edit().putString(KEY_LAST_HASH, mConfigHash).apply();
}
@Nullable
public String getClientId() {
return mClientId;
}
@NonNull
public String getScope() {
return mScope;
}
@NonNull
public Uri getRedirectUri() {
return mRedirectUri;
}
@Nullable
public Uri getDiscoveryUri() {
return mDiscoveryUri;
}
@Nullable
public Uri getEndSessionRedirectUri() { return mEndSessionRedirectUri; }
@Nullable
public Uri getAuthEndpointUri() {
return mAuthEndpointUri;
}
@Nullable
public Uri getTokenEndpointUri() {
return mTokenEndpointUri;
}
@Nullable
public Uri getEndSessionEndpoint() {
return mEndSessionEndpoint;
}
@Nullable
public Uri getRegistrationEndpointUri() {
return mRegistrationEndpointUri;
}
@Nullable
public Uri getUserInfoEndpointUri() {
return mUserInfoEndpointUri;
}
public boolean isHttpsRequired() {
return mHttpsRequired;
}
public ConnectionBuilder getConnectionBuilder() {
if (isHttpsRequired()) {
return DefaultConnectionBuilder.INSTANCE;
}
return ConnectionBuilderForTesting.INSTANCE;
}
private String getLastKnownConfigHash() {
return mPrefs.getString(KEY_LAST_HASH, null);
}
private void readConfiguration() throws InvalidConfigurationException {
BufferedSource configSource =
Okio.buffer(Okio.source(mResources.openRawResource(R.raw.auth_config)));
Buffer configData = new Buffer();
try {
configSource.readAll(configData);
mConfigJson = new JSONObject(configData.readString(Charset.forName("UTF-8")));
} catch (IOException ex) {
throw new InvalidConfigurationException(
"Failed to read configuration: " + ex.getMessage());
} catch (JSONException ex) {
throw new InvalidConfigurationException(
"Unable to parse configuration: " + ex.getMessage());
}
mConfigHash = configData.sha256().base64();
mClientId = getConfigString("client_id");
mScope = getRequiredConfigString("authorization_scope");
mRedirectUri = getRequiredConfigUri("redirect_uri");
mEndSessionRedirectUri = getRequiredConfigUri("end_session_redirect_uri");
if (!isRedirectUriRegistered()) {
throw new InvalidConfigurationException(
"redirect_uri is not handled by any activity in this app! "
+ "Ensure that the appAuthRedirectScheme in your build.gradle file "
+ "is correctly configured, or that an appropriate intent filter "
+ "exists in your app manifest.");
}
if (getConfigString("discovery_uri") == null) {
mAuthEndpointUri = getRequiredConfigWebUri("authorization_endpoint_uri");
mTokenEndpointUri = getRequiredConfigWebUri("token_endpoint_uri");
mUserInfoEndpointUri = getRequiredConfigWebUri("user_info_endpoint_uri");
mEndSessionEndpoint = getRequiredConfigUri("end_session_endpoint");
if (mClientId == null) {
mRegistrationEndpointUri = getRequiredConfigWebUri("registration_endpoint_uri");
}
} else {
mDiscoveryUri = getRequiredConfigWebUri("discovery_uri");
}
mHttpsRequired = mConfigJson.optBoolean("https_required", true);
}
@Nullable
String getConfigString(String propName) {
String value = mConfigJson.optString(propName);
if (value == null) {
return null;
}
value = value.trim();
if (TextUtils.isEmpty(value)) {
return null;
}
return value;
}
@NonNull
private String getRequiredConfigString(String propName)
throws InvalidConfigurationException {
String value = getConfigString(propName);
if (value == null) {
throw new InvalidConfigurationException(
propName + " is required but not specified in the configuration");
}
return value;
}
@NonNull
Uri getRequiredConfigUri(String propName)
throws InvalidConfigurationException {
String uriStr = getRequiredConfigString(propName);
Uri uri;
try {
uri = Uri.parse(uriStr);
} catch (Throwable ex) {
throw new InvalidConfigurationException(propName + " could not be parsed", ex);
}
if (!uri.isHierarchical() || !uri.isAbsolute()) {
throw new InvalidConfigurationException(
propName + " must be hierarchical and absolute");
}
if (!TextUtils.isEmpty(uri.getEncodedUserInfo())) {
throw new InvalidConfigurationException(propName + " must not have user info");
}
if (!TextUtils.isEmpty(uri.getEncodedQuery())) {
throw new InvalidConfigurationException(propName + " must not have query parameters");
}
if (!TextUtils.isEmpty(uri.getEncodedFragment())) {
throw new InvalidConfigurationException(propName + " must not have a fragment");
}
return uri;
}
Uri getRequiredConfigWebUri(String propName)
throws InvalidConfigurationException {
Uri uri = getRequiredConfigUri(propName);
String scheme = uri.getScheme();
if (TextUtils.isEmpty(scheme) || !("http".equals(scheme) || "https".equals(scheme))) {
throw new InvalidConfigurationException(
propName + " must have an http or https scheme");
}
return uri;
}
private boolean isRedirectUriRegistered() {
// ensure that the redirect URI declared in the configuration is handled by some activity
// in the app, by querying the package manager speculatively
Intent redirectIntent = new Intent();
redirectIntent.setPackage(mContext.getPackageName());
redirectIntent.setAction(Intent.ACTION_VIEW);
redirectIntent.addCategory(Intent.CATEGORY_BROWSABLE);
redirectIntent.setData(mRedirectUri);
return !mContext.getPackageManager().queryIntentActivities(redirectIntent, 0).isEmpty();
}
public static final class InvalidConfigurationException extends Exception {
InvalidConfigurationException(String reason) {
super(reason);
}
InvalidConfigurationException(String reason, Throwable cause) {
super(reason, cause);
}
}
}

12
res/raw/auth_config.json Normal file
View File

@ -0,0 +1,12 @@
{
"client_id": "165555585193-ud80sst45po348ohec2h33t2m6mjnlt0.apps.googleusercontent.com",
"redirect_uri": "com.googleusercontent.apps.165555585193-ud80sst45po348ohec2h33t2m6mjnlt0:/oauth2redirect",
"end_session_redirect_uri": "com.googleusercontent.apps.165555585193-ud80sst45po348ohec2h33t2m6mjnlt0:/oauth2redirect",
"authorization_scope": "openid email profile https://www.googleapis.com/auth/drive.appdata",
"discovery_uri": "https://accounts.google.com/.well-known/openid-configuration",
"authorization_endpoint_uri": "",
"token_endpoint_uri": "",
"registration_endpoint_uri": "",
"user_info_endpoint_uri": "",
"https_required": true
}

View File

@ -2,5 +2,5 @@
<string name="app_name">HeadlessCocos</string>
<string name="game_view_content_description">Game view</string>
<string name="permission_camera">Scan QRCode need camera permission</string>
<string name="default_web_client_id">165555585193-ud80sst45po348ohec2h33t2m6mjnlt0.apps.googleusercontent.com</string>
<string name="default_web_client_id">165555585193-j22geb66hjku5ns5lq5voaj4v7811cl7.apps.googleusercontent.com</string>
</resources>