增加google第三方登录相关代码
This commit is contained in:
parent
8db5491391
commit
5fd6c4a682
17
.idea/deploymentTargetDropDown.xml
generated
Normal file
17
.idea/deploymentTargetDropDown.xml
generated
Normal 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
1
.idea/gradle.xml
generated
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.fitchgc.headlesscocos">
|
package="com.fitchgc.headlesscocos">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@ -14,6 +15,7 @@
|
|||||||
android:value="cocos2djs" />
|
android:value="cocos2djs" />
|
||||||
<activity android:name=".MainActivity"
|
<activity android:name=".MainActivity"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="sensorLandscape"
|
||||||
|
android:exported="true"
|
||||||
>
|
>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@ -26,6 +28,32 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.king.zxing.CaptureActivity"
|
android:name="com.king.zxing.CaptureActivity"
|
||||||
android:theme="@style/CaptureTheme"/>
|
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>
|
</application>
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:anyDensity="true"
|
android:anyDensity="true"
|
||||||
|
@ -39,6 +39,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
manifestPlaceholders = [
|
||||||
|
'appAuthRedirectScheme': 'com.googleusercontent.apps.165555585193-ud80sst45po348ohec2h33t2m6mjnlt0'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets.main {
|
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 {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
|
signingConfig signingConfigs.release
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,4 +136,6 @@ dependencies {
|
|||||||
implementation 'pub.devrel:easypermissions:3.0.0'
|
implementation 'pub.devrel:easypermissions:3.0.0'
|
||||||
implementation 'com.github.jenly1314:zxing-lite:2.1.1'
|
implementation 'com.github.jenly1314:zxing-lite:2.1.1'
|
||||||
implementation project(path: ':libcocos2dx')
|
implementation project(path: ':libcocos2dx')
|
||||||
|
implementation 'net.openid:appauth:0.11.1'
|
||||||
|
implementation "com.squareup.okio:okio:2.10.0"
|
||||||
}
|
}
|
@ -4,40 +4,68 @@ import android.Manifest;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentSender;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
|
|
||||||
import com.google.android.gms.auth.api.identity.BeginSignInRequest;
|
import com.google.android.gms.auth.api.signin.GoogleSignIn;
|
||||||
import com.google.android.gms.auth.api.identity.BeginSignInResult;
|
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
|
||||||
import com.google.android.gms.auth.api.identity.Identity;
|
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
|
||||||
import com.google.android.gms.auth.api.identity.SignInClient;
|
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
|
||||||
import com.google.android.gms.auth.api.identity.SignInCredential;
|
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.ApiException;
|
||||||
import com.google.android.gms.common.api.CommonStatusCodes;
|
import com.google.android.gms.common.api.Scope;
|
||||||
import com.google.android.gms.tasks.OnFailureListener;
|
import com.google.android.gms.tasks.Task;
|
||||||
import com.google.android.gms.tasks.OnSuccessListener;
|
|
||||||
import com.jc.jcfw.JcSDK;
|
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.CameraScan;
|
||||||
import com.king.zxing.CaptureActivity;
|
import com.king.zxing.CaptureActivity;
|
||||||
import com.king.zxing.util.CodeUtils;
|
import com.king.zxing.util.CodeUtils;
|
||||||
import com.king.zxing.util.LogUtils;
|
import com.king.zxing.util.LogUtils;
|
||||||
import com.unity3d.player.UnityPlayer;
|
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.Cocos2dxHelper;
|
||||||
import org.cocos2dx.lib.CocosJSHelper;
|
import org.cocos2dx.lib.CocosJSHelper;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
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.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent;
|
||||||
import androidx.core.app.ActivityOptionsCompat;
|
import androidx.core.app.ActivityOptionsCompat;
|
||||||
import pub.devrel.easypermissions.AfterPermissionGranted;
|
import pub.devrel.easypermissions.AfterPermissionGranted;
|
||||||
import pub.devrel.easypermissions.EasyPermissions;
|
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_QR_CODE = "key_code";
|
||||||
public static final String KEY_IS_CONTINUOUS = "key_continuous_scan";
|
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_SCAN = 0X01;
|
||||||
public static final int REQUEST_CODE_PHOTO = 0X02;
|
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;
|
public static final int RC_CAMERA = 0X01;
|
||||||
|
|
||||||
@ -64,8 +96,22 @@ public class MainActivity extends Activity
|
|||||||
private String title;
|
private String title;
|
||||||
private String funId;
|
private String funId;
|
||||||
|
|
||||||
private SignInClient oneTapClient;
|
//AppAuth
|
||||||
private BeginSignInRequest signInRequest;
|
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) {
|
protected String updateUnityCommandLineArguments(String cmdLine) {
|
||||||
return cmdLine;
|
return cmdLine;
|
||||||
@ -89,21 +135,19 @@ public class MainActivity extends Activity
|
|||||||
Cocos2dxHelper.init(this);
|
Cocos2dxHelper.init(this);
|
||||||
CocosJSHelper.initJSEnv(getApplicationContext());
|
CocosJSHelper.initJSEnv(getApplicationContext());
|
||||||
|
|
||||||
oneTapClient = Identity.getSignInClient(this);
|
// begin of google sign
|
||||||
signInRequest = BeginSignInRequest.builder()
|
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||||||
.setPasswordRequestOptions(BeginSignInRequest.PasswordRequestOptions.builder()
|
.requestIdToken(getString(R.string.default_web_client_id))
|
||||||
.setSupported(true)
|
.requestScopes(new Scope("https://www.googleapis.com/auth/drive.appdata"))
|
||||||
.build())
|
.build();
|
||||||
.setGoogleIdTokenRequestOptions(BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
|
mGoogleSignInClient = GoogleSignIn.getClient(this, gso);
|
||||||
.setSupported(true)
|
// end of google sign
|
||||||
// Your server's client ID, not your Android client ID.
|
|
||||||
.setServerClientId(getString(R.string.default_web_client_id))
|
mExecutor = Executors.newSingleThreadExecutor();
|
||||||
// Only show accounts previously used to sign in.
|
mAuthStateManager = AuthStateManager.getInstance(this);
|
||||||
.setFilterByAuthorizedAccounts(true)
|
mConfiguration = JConfiguration.getInstance(this);
|
||||||
.build())
|
|
||||||
// Automatically sign in when exactly one credential is retrieved.
|
mExecutor.submit(this::initializeAppAuth);
|
||||||
.setAutoSelectEnabled(true)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -121,38 +165,26 @@ public class MainActivity extends Activity
|
|||||||
case REQUEST_CODE_PHOTO:
|
case REQUEST_CODE_PHOTO:
|
||||||
parsePhoto(data);
|
parsePhoto(data);
|
||||||
break;
|
break;
|
||||||
case REQ_ONE_TAP:
|
case RC_AUTH:
|
||||||
try {
|
AuthorizationResponse response = AuthorizationResponse.fromIntent(data);
|
||||||
SignInCredential credential = oneTapClient.getSignInCredentialFromIntent(data);
|
AuthorizationException ex = AuthorizationException.fromIntent(data);
|
||||||
String idToken = credential.getGoogleIdToken();
|
if (response != null || ex != null) {
|
||||||
String username = credential.getId();
|
mAuthStateManager.updateAfterAuthorization(response, ex);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -169,6 +201,10 @@ public class MainActivity extends Activity
|
|||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
mUnityPlayer.start();
|
mUnityPlayer.start();
|
||||||
|
|
||||||
|
if (mExecutor.isShutdown()) {
|
||||||
|
mExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume Unity
|
// Resume Unity
|
||||||
@ -375,32 +411,294 @@ public class MainActivity extends Activity
|
|||||||
// end of qrcode
|
// end of qrcode
|
||||||
|
|
||||||
public void signWithGoogle(String funId) {
|
public void signWithGoogle(String funId) {
|
||||||
oneTapClient.beginSignIn(signInRequest)
|
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) {
|
||||||
.addOnSuccessListener(this, new OnSuccessListener<BeginSignInResult>() {
|
GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
|
||||||
@Override
|
if (account != null) {
|
||||||
public void onSuccess(BeginSignInResult result) {
|
Log.w(TAG, "already login: " + account.getIdToken());
|
||||||
try {
|
} else {
|
||||||
startIntentSenderForResult(
|
Intent signInIntent = mGoogleSignInClient.getSignInIntent();
|
||||||
result.getPendingIntent().getIntentSender(), REQ_ONE_TAP,
|
startActivityForResult(signInIntent, RC_SIGN_IN);
|
||||||
null, 0, 0, 0);
|
}
|
||||||
} catch (IntentSender.SendIntentException e) {
|
} else {
|
||||||
Log.e(TAG, "Couldn't start One Tap UI: " + e.getLocalizedMessage());
|
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());
|
||||||
.addOnFailureListener(this, new OnFailureListener() {
|
} else {
|
||||||
@Override
|
mExecutor.submit(this::doAuth);
|
||||||
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());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void signOutGoogle(String funId) {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
150
app/src/com/jc/jcfw/appauth/AuthStateManager.java
Normal file
150
app/src/com/jc/jcfw/appauth/AuthStateManager.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
app/src/com/jc/jcfw/appauth/ConnectionBuilderForTesting.java
Normal file
108
app/src/com/jc/jcfw/appauth/ConnectionBuilderForTesting.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
310
app/src/com/jc/jcfw/appauth/JConfiguration.java
Normal file
310
app/src/com/jc/jcfw/appauth/JConfiguration.java
Normal 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
12
res/raw/auth_config.json
Normal 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
|
||||||
|
}
|
@ -2,5 +2,5 @@
|
|||||||
<string name="app_name">HeadlessCocos</string>
|
<string name="app_name">HeadlessCocos</string>
|
||||||
<string name="game_view_content_description">Game view</string>
|
<string name="game_view_content_description">Game view</string>
|
||||||
<string name="permission_camera">Scan QRCode need camera permission</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>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user