Skip to content

Commit b8242c7

Browse files
authored
feat(auth): Prefer Private Session support for WebUI Sign In's (#3108)
1 parent 67346c4 commit b8242c7

File tree

10 files changed

+224
-19
lines changed

10 files changed

+224
-19
lines changed

aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/HostedUIClient.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ internal class HostedUIClient private constructor(
6363
launchCustomTabs(
6464
uri = createAuthorizeUri(hostedUIOptions),
6565
activity = hostedUIOptions.callingActivity,
66-
customBrowserPackage = hostedUIOptions.browserPackage
66+
customBrowserPackage = hostedUIOptions.browserPackage,
67+
preferPrivateSession = hostedUIOptions.preferPrivateSession
6768
)
6869
}
6970

@@ -75,14 +76,22 @@ internal class HostedUIClient private constructor(
7576
)
7677
}
7778

78-
private fun launchCustomTabs(uri: Uri, activity: Activity? = null, customBrowserPackage: String?) {
79+
private fun launchCustomTabs(
80+
uri: Uri,
81+
activity: Activity? = null,
82+
customBrowserPackage: String?,
83+
preferPrivateSession: Boolean? = null // allowing nullable, as null means customer didn't specify
84+
) {
7985
if (!BrowserHelper.isBrowserInstalled(context)) {
8086
throw RuntimeException("No browsers installed")
8187
}
8288

8389
val browserPackage = customBrowserPackage ?: defaultCustomTabsPackage
8490

85-
val customTabsIntent = CustomTabsIntent.Builder(session).build().apply {
91+
val customTabsIntent = CustomTabsIntent.Builder(session).apply {
92+
// If customer didn't specify (null), don't add any Intent extra
93+
preferPrivateSession?.let { setEphemeralBrowsingEnabled(it) }
94+
}.build().apply {
8695
browserPackage?.let { intent.`package` = it }
8796
intent.data = uri
8897
}

aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/HostedUIHelper.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ internal object HostedUIHelper {
3535
authProvider = authProvider,
3636
idpIdentifier = (options as? AWSCognitoAuthWebUISignInOptions)?.idpIdentifier
3737
),
38-
browserPackage = (options as? AWSCognitoAuthWebUISignInOptions)?.browserPackage
38+
browserPackage = (options as? AWSCognitoAuthWebUISignInOptions)?.browserPackage,
39+
preferPrivateSession = options.preferPrivateSession
3940
)
4041

4142
/**

aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/options/AWSCognitoAuthWebUISignInOptions.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@ public final class AWSCognitoAuthWebUISignInOptions extends AuthWebUISignInOptio
3737
* @param idpIdentifier The IdentityProvider identifier if using multiple instances of same identity provider.
3838
* @param browserPackage Specify which browser package should be used for web sign in (e.g. "org.mozilla.firefox").
3939
* Defaults to the Chrome package if not specified.
40+
* @param preferPrivateSession specifying whether or not to launch web ui in an ephemeral CustomTab.
4041
*/
4142
protected AWSCognitoAuthWebUISignInOptions(
4243
List<String> scopes,
4344
String idpIdentifier,
44-
String browserPackage
45+
String browserPackage,
46+
Boolean preferPrivateSession
4547
) {
46-
super(scopes);
48+
super(scopes, preferPrivateSession);
4749
this.idpIdentifier = idpIdentifier;
4850
this.browserPackage = browserPackage;
4951
}
@@ -80,7 +82,8 @@ public int hashCode() {
8082
return ObjectsCompat.hash(
8183
getScopes(),
8284
getIdpIdentifier(),
83-
getBrowserPackage()
85+
getBrowserPackage(),
86+
getPreferPrivateSession()
8487
);
8588
}
8689

@@ -94,7 +97,8 @@ public boolean equals(Object obj) {
9497
AWSCognitoAuthWebUISignInOptions webUISignInOptions = (AWSCognitoAuthWebUISignInOptions) obj;
9598
return ObjectsCompat.equals(getScopes(), webUISignInOptions.getScopes()) &&
9699
ObjectsCompat.equals(getIdpIdentifier(), webUISignInOptions.getIdpIdentifier()) &&
97-
ObjectsCompat.equals(getBrowserPackage(), webUISignInOptions.getBrowserPackage());
100+
ObjectsCompat.equals(getBrowserPackage(), webUISignInOptions.getBrowserPackage()) &&
101+
ObjectsCompat.equals(getPreferPrivateSession(), webUISignInOptions.getPreferPrivateSession());
98102
}
99103
}
100104

@@ -104,6 +108,7 @@ public String toString() {
104108
"scopes=" + getScopes() +
105109
", idpIdentifier=" + getIdpIdentifier() +
106110
", browserPackage=" + getBrowserPackage() +
111+
", preferPrivateSession=" + getPreferPrivateSession() +
107112
'}';
108113
}
109114

@@ -162,7 +167,8 @@ public AWSCognitoAuthWebUISignInOptions build() {
162167
return new AWSCognitoAuthWebUISignInOptions(
163168
Immutable.of(super.getScopes()),
164169
idpIdentifier,
165-
browserPackage
170+
browserPackage,
171+
super.getPreferPrivateSession()
166172
);
167173
}
168174
}

aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/HostedUIOptions.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ internal data class HostedUIOptions(
2121
val callingActivity: Activity,
2222
val scopes: List<String>?,
2323
val providerInfo: HostedUIProviderInfo,
24-
val browserPackage: String?
24+
val browserPackage: String?,
25+
val preferPrivateSession: Boolean?
2526
)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.auth.cognito.helpers
17+
18+
import android.app.Activity
19+
import com.amplifyframework.auth.AuthProvider
20+
import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions
21+
import io.kotest.matchers.shouldBe
22+
import io.mockk.mockk
23+
import org.junit.Test
24+
25+
class HostedUIHelperTest {
26+
27+
private val mockActivity = mockk<Activity>()
28+
29+
@Test
30+
fun `createHostedUIOptions with AWSCognitoAuthWebUISignInOptions and preferPrivateSession true`() {
31+
val scopes = listOf("openid", "profile")
32+
val options = AWSCognitoAuthWebUISignInOptions.builder()
33+
.scopes(scopes)
34+
.browserPackage("com.android.chrome")
35+
.preferPrivateSession(true)
36+
.build()
37+
38+
val hostedUIOptions = HostedUIHelper.createHostedUIOptions(
39+
callingActivity = mockActivity,
40+
authProvider = AuthProvider.google(),
41+
options = options
42+
)
43+
44+
hostedUIOptions.callingActivity shouldBe mockActivity
45+
hostedUIOptions.scopes shouldBe scopes
46+
hostedUIOptions.browserPackage shouldBe "com.android.chrome"
47+
hostedUIOptions.preferPrivateSession shouldBe true
48+
}
49+
50+
@Test
51+
fun `createHostedUIOptions with AWSCognitoAuthWebUISignInOptions and preferPrivateSession false`() {
52+
val scopes = listOf("openid", "email")
53+
val options = AWSCognitoAuthWebUISignInOptions.builder()
54+
.scopes(scopes)
55+
.browserPackage("org.mozilla.firefox")
56+
.preferPrivateSession(false)
57+
.build()
58+
59+
val hostedUIOptions = HostedUIHelper.createHostedUIOptions(
60+
callingActivity = mockActivity,
61+
authProvider = AuthProvider.facebook(),
62+
options = options
63+
)
64+
65+
hostedUIOptions.callingActivity shouldBe mockActivity
66+
hostedUIOptions.scopes shouldBe scopes
67+
hostedUIOptions.browserPackage shouldBe "org.mozilla.firefox"
68+
hostedUIOptions.preferPrivateSession shouldBe false
69+
}
70+
71+
@Test
72+
fun `createHostedUIOptions with AWSCognitoAuthWebUISignInOptions and preferPrivateSession null`() {
73+
val scopes = listOf("openid")
74+
val options = AWSCognitoAuthWebUISignInOptions.builder()
75+
.scopes(scopes)
76+
.browserPackage("com.android.chrome")
77+
.build()
78+
79+
val hostedUIOptions = HostedUIHelper.createHostedUIOptions(
80+
callingActivity = mockActivity,
81+
authProvider = null,
82+
options = options
83+
)
84+
85+
hostedUIOptions.callingActivity shouldBe mockActivity
86+
hostedUIOptions.scopes shouldBe scopes
87+
hostedUIOptions.browserPackage shouldBe "com.android.chrome"
88+
hostedUIOptions.preferPrivateSession shouldBe null
89+
}
90+
91+
@Test
92+
fun `selectRedirectUri prefers non-HTTP scheme`() {
93+
val redirectUris = listOf(
94+
"https://example.com/callback",
95+
"myapp://auth/callback",
96+
"http://localhost:3000/callback"
97+
)
98+
99+
val selectedUri = HostedUIHelper.selectRedirectUri(redirectUris)
100+
selectedUri shouldBe "myapp://auth/callback"
101+
}
102+
103+
@Test
104+
fun `selectRedirectUri returns first URI when no non-HTTP scheme available`() {
105+
val redirectUris = listOf(
106+
"https://example.com/callback",
107+
"http://localhost:3000/callback"
108+
)
109+
110+
val selectedUri = HostedUIHelper.selectRedirectUri(redirectUris)
111+
selectedUri shouldBe "https://example.com/callback"
112+
}
113+
114+
@Test
115+
fun `selectRedirectUri returns null for empty list`() {
116+
val redirectUris = emptyList<String>()
117+
val selectedUri = HostedUIHelper.selectRedirectUri(redirectUris)
118+
selectedUri shouldBe null
119+
}
120+
}

aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/options/APIOptionsContractTest.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,34 @@ public void testCognitoOptions() {
9797
.scopes(scopes).build();
9898
Assert.assertEquals(webUISignInOptions.getBrowserPackage(), "chrome");
9999
Assert.assertEquals(webUISignInOptions.getScopes(), scopes);
100+
Assert.assertNull(webUISignInOptions.getPreferPrivateSession());
101+
102+
// Test preferPrivateSession option
103+
AWSCognitoAuthWebUISignInOptions webUISignInOptionsWithPrivateSession =
104+
AWSCognitoAuthWebUISignInOptions.builder()
105+
.browserPackage("chrome")
106+
.scopes(scopes)
107+
.preferPrivateSession(true)
108+
.build();
109+
Assert.assertEquals("chrome", webUISignInOptionsWithPrivateSession.getBrowserPackage());
110+
Assert.assertEquals(scopes, webUISignInOptionsWithPrivateSession.getScopes());
111+
Assert.assertEquals(Boolean.TRUE, webUISignInOptionsWithPrivateSession.getPreferPrivateSession());
112+
113+
// Test preferPrivateSession set to false
114+
AWSCognitoAuthWebUISignInOptions webUISignInOptionsWithoutPrivateSession =
115+
AWSCognitoAuthWebUISignInOptions.builder()
116+
.browserPackage("firefox")
117+
.scopes(scopes)
118+
.preferPrivateSession(false)
119+
.build();
120+
Assert.assertEquals("firefox", webUISignInOptionsWithoutPrivateSession.getBrowserPackage());
121+
Assert.assertEquals(scopes, webUISignInOptionsWithoutPrivateSession.getScopes());
122+
Assert.assertEquals(Boolean.FALSE, webUISignInOptionsWithoutPrivateSession.getPreferPrivateSession());
100123

101124
FederateToIdentityPoolOptions federateToIdentityPoolOptions =
102125
FederateToIdentityPoolOptions.builder().developerProvidedIdentityId("test-idp")
103126
.build();
104-
Assert.assertEquals(federateToIdentityPoolOptions
105-
.getDeveloperProvidedIdentityId(), "test-idp");
127+
Assert.assertEquals("test-idp", federateToIdentityPoolOptions
128+
.getDeveloperProvidedIdentityId());
106129
}
107130
}

build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
5858
}
5959

6060
extension.apply {
61-
compileSdk = 34
61+
compileSdk = 36
6262

6363
buildFeatures {
6464
buildConfig = true

core/api/core.api

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1094,9 +1094,10 @@ public final class com/amplifyframework/auth/options/AuthVerifyTOTPSetupOptions$
10941094
}
10951095

10961096
public class com/amplifyframework/auth/options/AuthWebUISignInOptions {
1097-
protected fun <init> (Ljava/util/List;)V
1097+
protected fun <init> (Ljava/util/List;Ljava/lang/Boolean;)V
10981098
public static fun builder ()Lcom/amplifyframework/auth/options/AuthWebUISignInOptions$Builder;
10991099
public fun equals (Ljava/lang/Object;)Z
1100+
public fun getPreferPrivateSession ()Ljava/lang/Boolean;
11001101
public fun getScopes ()Ljava/util/List;
11011102
public fun hashCode ()I
11021103
public fun toString ()Ljava/lang/String;
@@ -1105,8 +1106,10 @@ public class com/amplifyframework/auth/options/AuthWebUISignInOptions {
11051106
public abstract class com/amplifyframework/auth/options/AuthWebUISignInOptions$Builder {
11061107
public fun <init> ()V
11071108
public fun build ()Lcom/amplifyframework/auth/options/AuthWebUISignInOptions;
1109+
public fun getPreferPrivateSession ()Ljava/lang/Boolean;
11081110
public fun getScopes ()Ljava/util/List;
11091111
public abstract fun getThis ()Lcom/amplifyframework/auth/options/AuthWebUISignInOptions$Builder;
1112+
public fun preferPrivateSession (Ljava/lang/Boolean;)Lcom/amplifyframework/auth/options/AuthWebUISignInOptions$Builder;
11101113
public fun scopes (Ljava/util/List;)Lcom/amplifyframework/auth/options/AuthWebUISignInOptions$Builder;
11111114
}
11121115

0 commit comments

Comments
 (0)