Skip to content

Commit

Permalink
Merge pull request #406 from ForgeRock/SDKS-3079
Browse files Browse the repository at this point in the history
SDKS-3079 Replace deprecated onActivityResult with ActivityResultContract
  • Loading branch information
spetrov authored Apr 2, 2024
2 parents 1e1dd90 + 5787425 commit 4444228
Show file tree
Hide file tree
Showing 13 changed files with 378 additions and 48 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#### Fixed
- Addressed `nimbus-jose-jwt:9.25` library security vulnerability (CVE-2023-52428) [SDKS-2988]
- NullPointerException for Centralize Login, Replace deprecated onActivityResult with ActivityResultContract [SDKS-3079]

## [4.3.1]
#### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020 ForgeRock. All rights reserved.
* Copyright (c) 2020-2024 ForgeRock. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand All @@ -20,8 +20,6 @@

import java.net.MalformedURLException;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

/**
Expand All @@ -33,16 +31,12 @@ public class AppAuthConfigurer {

private final FRUser.Browser parent;

@Getter(AccessLevel.PACKAGE)
private Consumer<AuthorizationRequest.Builder> authorizationRequestBuilder = builder -> {
};
@Getter(AccessLevel.PACKAGE)
private androidx.core.util.Consumer<AppAuthConfiguration.Builder> appAuthConfigurationBuilder = builder -> {
};
@Getter(AccessLevel.PACKAGE)
private Consumer<CustomTabsIntent.Builder> customTabsIntentBuilder = builder -> {
};
@Getter(AccessLevel.PACKAGE)
private Supplier<AuthorizationServiceConfiguration> authorizationServiceConfigurationSupplier = () -> {
OAuth2Client oAuth2Client = Config.getInstance().getOAuth2Client();
try {
Expand Down Expand Up @@ -112,4 +106,19 @@ public FRUser.Browser done() {
return parent;
}

Consumer<AuthorizationRequest.Builder> getAuthorizationRequestBuilder() {
return this.authorizationRequestBuilder;
}

Consumer<AppAuthConfiguration.Builder> getAppAuthConfigurationBuilder() {
return this.appAuthConfigurationBuilder;
}

Consumer<CustomTabsIntent.Builder> getCustomTabsIntentBuilder() {
return this.customTabsIntentBuilder;
}

Supplier<AuthorizationServiceConfiguration> getAuthorizationServiceConfigurationSupplier() {
return this.authorizationServiceConfigurationSupplier;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
/**
* Headless Fragment to receive callback result from AppAuth library
*/
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class AppAuthFragment extends Fragment {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2024 ForgeRock. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
package org.forgerock.android.auth

import android.content.Intent
import android.os.Bundle
import androidx.annotation.RestrictTo
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.flow.MutableStateFlow
import net.openid.appauth.AuthorizationResponse
import org.forgerock.android.auth.centralize.BrowserLauncher
import org.forgerock.android.auth.centralize.Launcher

private const val PENDING = "pending"

/**
* Headless Fragment to receive callback result from AppAuth library
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
class AppAuthFragment2 : Fragment() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val state: MutableStateFlow<Intent?> = MutableStateFlow(null)
val delegate =
registerForActivityResult(AuthorizeContract()) {
state.value = it
}

val pending = savedInstanceState?.getBoolean(PENDING, false) ?: false

BrowserLauncher.init(Launcher(delegate, state, pending))
}

override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(PENDING, true)
super.onSaveInstanceState(outState)
}

override fun onDestroy() {
super.onDestroy()
BrowserLauncher.reset()
}

companion object {
val TAG: String = AppAuthFragment2::class.java.name

/**
* Initialize the Fragment to receive AppAuth callback event.
*/
@Synchronized
@JvmStatic
fun launch(activity: FragmentActivity, browser: FRUser.Browser) {
val fragmentManager: FragmentManager = activity.supportFragmentManager
var current = fragmentManager.findFragmentByTag(TAG) as? AppAuthFragment2
if (current == null) {
current = AppAuthFragment2()
fragmentManager.beginTransaction().add(current, TAG).commitNow()
}

BrowserLauncher.authorize(browser, object : FRListener<AuthorizationResponse> {
override fun onSuccess(result: AuthorizationResponse) {
reset(activity, current)
browser.listener.onSuccess(result)
}

override fun onException(e: Exception) {
reset(activity, current)
browser.listener.onException(e)
}

/**
* Once receive the result, reset state.
*/
private fun reset(activity: FragmentActivity, fragment: Fragment?) {
activity.runOnUiThread {
BrowserLauncher.reset()
}
fragment?.let {
activity.runOnUiThread {
fragmentManager.beginTransaction().remove(it).commitNow()
}
}
}
})


}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2024 ForgeRock. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/

package org.forgerock.android.auth

import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
import androidx.browser.customtabs.CustomTabsIntent
import net.openid.appauth.AppAuthConfiguration
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationService
import org.forgerock.android.auth.FRUser.Browser

/**
* This class is an implementation of the ActivityResultContract.
* It is used to handle the OAuth2 authorization process.
*/
internal class AuthorizeContract : ActivityResultContract<Browser, Intent?>() {
override fun createIntent(
context: Context,
input: Browser,
): Intent {
val configurer: AppAuthConfigurer = input.appAuthConfigurer
val oAuth2Client = Config.getInstance().oAuth2Client

val configuration = configurer.authorizationServiceConfigurationSupplier.get()
val authRequestBuilder =
AuthorizationRequest.Builder(
configuration,
oAuth2Client.clientId,
oAuth2Client.responseType,
Uri.parse(oAuth2Client.redirectUri),
).setScope(oAuth2Client.scope)

//Allow caller to override Authorization Request setting
configurer.authorizationRequestBuilder.accept(authRequestBuilder)
val authorizationRequest = authRequestBuilder.build()

//Allow caller to override AppAuth default setting
val appAuthConfigurationBuilder = AppAuthConfiguration.Builder()
configurer.appAuthConfigurationBuilder.accept(appAuthConfigurationBuilder)
val authorizationService = AuthorizationService(context, appAuthConfigurationBuilder.build())

//Allow caller to override custom tabs default setting
val intentBuilder: CustomTabsIntent.Builder =
authorizationService.createCustomTabsIntentBuilder(authorizationRequest.toUri())
configurer.customTabsIntentBuilder.accept(intentBuilder)

val request = authRequestBuilder.build()
val service = AuthorizationService(context, AppAuthConfiguration.DEFAULT)
return service.getAuthorizationRequestIntent(request, intentBuilder.build())
}

override fun parseResult(
resultCode: Int,
intent: Intent?,
): Intent? {
return intent
}
}
58 changes: 31 additions & 27 deletions forgerock-auth/src/main/java/org/forgerock/android/auth/FRUser.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 - 2023 ForgeRock. All rights reserved.
* Copyright (c) 2019 - 2024 ForgeRock. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand All @@ -14,15 +14,16 @@
import android.content.pm.ResolveInfo;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;

import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.RedirectUriReceiverActivity;

import org.forgerock.android.auth.centralize.BrowserLauncher;
import org.forgerock.android.auth.exception.AlreadyAuthenticatedException;
import org.forgerock.android.auth.exception.AuthenticationRequiredException;
import org.forgerock.android.auth.exception.InvalidRedirectUriException;
Expand All @@ -31,8 +32,6 @@
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

public class FRUser {
Expand Down Expand Up @@ -99,7 +98,7 @@ public void revokeAccessToken(FRListener<Void> listener) {
* Refresh the {@link AccessToken} asynchronously, force token refresh, no matter the stored {@link AccessToken} is expired or not
* refresh the token and persist it.
*
* @param listener Listener to listen for refresh event.
* @param listener Listener to listen for refresh event.
*/
@WorkerThread
public void refresh(FRListener<AccessToken> listener) {
Expand Down Expand Up @@ -256,10 +255,10 @@ public static Browser browser() {
return new Browser();
}

@Getter(AccessLevel.PACKAGE)
public static class Browser {

public FRListener<AuthorizationResponse> listener;
private static final String TAG = Browser.class.getName();
private FRListener<AuthorizationResponse> listener;
private AppAuthConfigurer appAuthConfigurer = new AppAuthConfigurer(this);
private boolean failedOnNoBrowserFound = true;

Expand All @@ -281,7 +280,7 @@ public AppAuthConfigurer appAuthConfigurer() {
* <b> throws {@link java.net.MalformedURLException} When failed to parse the URL for API request.
*/
public void login(Fragment fragment, FRListener<FRUser> listener) {
login(fragment.getContext(), fragment.getFragmentManager(), listener);
login(fragment.getActivity(), listener);
}

/**
Expand All @@ -298,17 +297,6 @@ public void login(Fragment fragment, FRListener<FRUser> listener) {
* <b> throws {@link java.net.MalformedURLException} When failed to parse the URL for API request.
*/
public void login(FragmentActivity activity, FRListener<FRUser> listener) {
login(activity.getApplicationContext(), activity.getSupportFragmentManager(), listener);
}

@VisibleForTesting
Browser failedOnNoBrowserFound(boolean failedOnNoBrowserFound) {
this.failedOnNoBrowserFound = failedOnNoBrowserFound;
return this;
}

private void login(Context context, FragmentManager manager, FRListener<FRUser> listener) {

SessionManager sessionManager = Config.getInstance().getSessionManager();

if (sessionManager.hasSession()) {
Expand All @@ -317,35 +305,33 @@ private void login(Context context, FragmentManager manager, FRListener<FRUser>
}

try {
validateRedirectUri(context);
validateRedirectUri(activity);
} catch (InvalidRedirectUriException e) {
Listener.onException(listener, e);
return;
}

this.listener = new FRListener<AuthorizationResponse>() {
this.listener = new FRListener<>() {
@Override
public void onSuccess(AuthorizationResponse result) {
InterceptorHandler interceptorHandler = InterceptorHandler.builder()
.context(context)
.context(activity)
.listener(listener)
.interceptor(new ExchangeAccessTokenInterceptor(sessionManager.getTokenManager()))
.interceptor(new AccessTokenStoreInterceptor(sessionManager.getTokenManager()))
.interceptor(new UserInterceptor())
.build();

interceptorHandler.proceed(result);

}

@Override
public void onException(Exception e) {
public void onException(@NonNull Exception e) {
Listener.onException(listener, e);
}
};

AppAuthFragment.launch(manager, this);

AppAuthFragment2.launch(activity, this);
}

private void validateRedirectUri(Context context) throws InvalidRedirectUriException {
Expand All @@ -359,7 +345,7 @@ private void validateRedirectUri(Context context) throws InvalidRedirectUriExcep
intent.setData(uri);
resolveInfos = pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);
}
if (resolveInfos != null && resolveInfos.size() > 0) {
if (resolveInfos != null && !resolveInfos.isEmpty()) {
for (ResolveInfo info : resolveInfos) {
ActivityInfo activityInfo = info.activityInfo;
if (!(activityInfo.name.equals(RedirectUriReceiverActivity.class.getCanonicalName()) &&
Expand All @@ -371,5 +357,23 @@ private void validateRedirectUri(Context context) throws InvalidRedirectUriExcep
}
throw new InvalidRedirectUriException("No App is registered to capture the authorization code");
}

@VisibleForTesting
Browser failedOnNoBrowserFound(boolean failedOnNoBrowserFound) {
this.failedOnNoBrowserFound = failedOnNoBrowserFound;
return this;
}

FRListener<AuthorizationResponse> getListener() {
return this.listener;
}

AppAuthConfigurer getAppAuthConfigurer() {
return this.appAuthConfigurer;
}

boolean isFailedOnNoBrowserFound() {
return this.failedOnNoBrowserFound;
}
}
}
Loading

0 comments on commit 4444228

Please sign in to comment.