package com.cege.games.release.appauth; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.text.TextUtils; import android.util.Log; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.browser.customtabs.CustomTabsIntent; import com.jc.jcfw.JcSDK; import com.jc.jcfw.appauth.AuthStateManager; import com.jc.jcfw.appauth.JConfiguration; import com.jc.jcfw.util.ThreadUtils; 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 java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; public class AppAuthSvr { private final String TAG = getClass().getSimpleName(); private final Activity activity; private AuthorizationService mAuthService; private AuthStateManager mAuthStateManager; private JConfiguration mConfiguration; private ExecutorService mExecutor; private final AtomicReference mClientId = new AtomicReference<>(); private final AtomicReference mAuthRequest = new AtomicReference<>(); private final AtomicReference mAuthIntent = new AtomicReference<>(); private CountDownLatch mAuthIntentLatch = new CountDownLatch(1); private String mFunID; private static final int RC_AUTH = 0X04; private Consumer loginCbAction = null; @NonNull private final BrowserMatcher mBrowserMatcher = AnyBrowserMatcher.INSTANCE; public AppAuthSvr(Activity activity) { this.activity = activity; } public void init(ExecutorService executorService) { mExecutor = executorService; mAuthStateManager = AuthStateManager.getInstance(activity); mConfiguration = JConfiguration.getInstance(activity); mExecutor.submit(this::initializeAppAuth); } public void setFunID(String funID) { this.mFunID = funID; } public void setLoginCbAction(Consumer func) { this.loginCbAction = func; } public AuthState getCurrentState() { return mAuthStateManager.getCurrent(); } /** * Performs the authorization request, using the browser selected in the * spinner, * and a user-provided `login_hint` if available. */ @WorkerThread public void doAuth(String funID, Consumer func) { this.mFunID = funID; this.loginCbAction = func; try { mAuthIntentLatch.await(); } catch (InterruptedException ex) { Log.w(TAG, "Interrupted while waiting for auth intent"); } Intent intent = mAuthService.getAuthorizationRequestIntent( mAuthRequest.get(), mAuthIntent.get()); activity.startActivityForResult(intent, RC_AUTH); } public void parseLoginResult(@NonNull Intent data) { 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()); errorCB("Authorization flow failed: " + ex.getMessage()); } else { Log.i(TAG, "No authorization state retained - reauthorization required"); errorCB("No authorization state retained - reauthorization required"); } } public void refreshToken(String funID, Consumer func) { this.mFunID = funID; this.loginCbAction = func; AuthState state = getCurrentState(); performTokenRequest(state.createTokenRefreshRequest(), this::handleCodeExchangeResponse); } /** * 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(activity, 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()) // apple need `form_post` when authorization_scope has `name email ` // .setResponseMode("form_post") // .setResponseType("code id_token") .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"); Uri uri = mAuthRequest.get().toUri(); Log.d(TAG, "URI: " + uri); CustomTabsIntent.Builder intentBuilder = mAuthService.createCustomTabsIntentBuilder(uri); 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()); ThreadUtils.runInMain(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); ThreadUtils.runInMain(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); } @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 : ""); Log.d(TAG, message); errorCB(message); } else { checkAuthStateAndCB(); } } private void checkAuthStateAndCB() { AuthState state = mAuthStateManager.getCurrent(); Log.d(TAG, "login success, auth state: " + state.isAuthorized()); Log.d(TAG, "id token : " + state.getIdToken()); Log.d(TAG, "access token: " + state.getAccessToken()); Log.d(TAG, "refresh token: " + state.getRefreshToken()); mAuthStateManager.replace(state); if (loginCbAction != null) { mExecutor.submit(() -> { loginCbAction.accept("success"); clearParams(); }); } else { successCB(state.getIdToken()); } } private void clearParams() { this.mFunID = null; this.loginCbAction = null; } public void errorCB(String msg) { JcSDK.nativeCb(mFunID, msg, null); clearParams(); } public void successCB(String dataStr) { JcSDK.nativeCb(mFunID, null, dataStr); clearParams(); } }