Skip to content

Commit 3cd5890

Browse files
committed
Add SilentAuthReceiver for Doze network-block reproduction
Add a BroadcastReceiver to MsalTestApp that triggers acquireTokenSilent from a background process context (PROCESS_STATE_RECEIVER). This enables reliable reproduction of the Doze network-block issue that affects background broker auth triggered by FCM notifications. Key findings from investigation: - When a foreground app binds to Broker via IPC, Android's NetworkPolicyManagerService adds a dozable-allow firewall rule for the Broker's UID. This masks the Doze network block during UI testing. - When the caller is in a background context (BroadcastReceiver/Service), the IPC binding does NOT elevate the Broker enough for dozable-allow. The Broker's network call fails with UnknownHostException. - This matches the production scenario: Outlook receives FCM push in background -> calls OneAuth -> OneAuth calls Broker -> Broker's network to eSTS is blocked by Doze firewall. Usage: adb shell dumpsys battery unplug adb shell dumpsys deviceidle force-idle adb shell am broadcast \ -a com.microsoft.identity.client.testapp.SILENT_AUTH \ -n com.msft.identity.client.sample.local/com.microsoft.identity.client.testapp.SilentAuthReceiver adb logcat -s SilentAuthReceiver:*
1 parent d003b0b commit 3cd5890

2 files changed

Lines changed: 203 additions & 0 deletions

File tree

testapps/testapp/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@
8787
android:path="/1wIqXSqBj7w+h11ZifsnqwgyKrY="/>
8888
</intent-filter>
8989
</activity>
90+
91+
<!-- Receiver for testing silent auth from background context (Doze repro) -->
92+
<receiver
93+
android:name="com.microsoft.identity.client.testapp.SilentAuthReceiver"
94+
android:exported="true">
95+
<intent-filter>
96+
<action android:name="com.microsoft.identity.client.testapp.SILENT_AUTH" />
97+
</intent-filter>
98+
</receiver>
9099
</application>
91100

92101
</manifest>
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.client.testapp;
24+
25+
import android.content.BroadcastReceiver;
26+
import android.content.Context;
27+
import android.content.Intent;
28+
import android.util.Log;
29+
30+
import com.microsoft.identity.client.AcquireTokenSilentParameters;
31+
import com.microsoft.identity.client.AuthenticationCallback;
32+
import com.microsoft.identity.client.IAccount;
33+
import com.microsoft.identity.client.IAuthenticationResult;
34+
import com.microsoft.identity.client.IMultipleAccountPublicClientApplication;
35+
import com.microsoft.identity.client.IPublicClientApplication;
36+
import com.microsoft.identity.client.ISingleAccountPublicClientApplication;
37+
import com.microsoft.identity.client.PublicClientApplication;
38+
import com.microsoft.identity.client.exception.MsalException;
39+
40+
import java.util.Arrays;
41+
import java.util.List;
42+
43+
/**
44+
* BroadcastReceiver that triggers acquireTokenSilent from a background context.
45+
*
46+
* This runs at PROCESS_STATE_RECEIVER priority, which does NOT elevate the
47+
* Broker process enough to get a dozable-allow firewall rule. This simulates
48+
* the production scenario where Outlook handles an FCM push in the background
49+
* and calls the Broker via OneAuth.
50+
*
51+
* Usage:
52+
* adb shell am broadcast \
53+
* -a com.microsoft.identity.client.testapp.SILENT_AUTH \
54+
* -n com.msft.identity.client.sample.local/com.microsoft.identity.client.testapp.SilentAuthReceiver \
55+
* --es scopes "https://graph.microsoft.com/.default"
56+
*/
57+
public class SilentAuthReceiver extends BroadcastReceiver {
58+
59+
private static final String TAG = "SilentAuthReceiver";
60+
public static final String ACTION_SILENT_AUTH =
61+
"com.microsoft.identity.client.testapp.SILENT_AUTH";
62+
63+
@Override
64+
public void onReceive(final Context context, final Intent intent) {
65+
Log.w(TAG, "=== SilentAuthReceiver triggered (PROCESS_STATE_RECEIVER) ===");
66+
67+
final String scopes = intent.getStringExtra("scopes");
68+
final String scopeString = (scopes != null) ? scopes : "https://graph.microsoft.com/.default";
69+
70+
Log.w(TAG, "Scopes: " + scopeString);
71+
72+
// Use goAsync() to extend the receiver's lifecycle beyond the 10s limit
73+
final PendingResult pendingResult = goAsync();
74+
75+
// Create PCA with default config (uses broker)
76+
final int configResourceId = Constants.getResourceIdFromConfigFile(Constants.ConfigFile.DEFAULT);
77+
78+
PublicClientApplication.create(context.getApplicationContext(),
79+
configResourceId,
80+
new PublicClientApplication.ApplicationCreatedListener() {
81+
@Override
82+
public void onCreated(IPublicClientApplication application) {
83+
// Force-disable powerOptCheck so the Broker attempts the real
84+
// network call instead of proactively blocking with a Doze check.
85+
// This matches production Authenticator behavior (which doesn't
86+
// have powerOptCheckEnabled set by OneAuth).
87+
application.getConfiguration().setPowerOptCheckEnabled(false);
88+
89+
Log.w(TAG, "PCA created, mode: " +
90+
(application instanceof ISingleAccountPublicClientApplication
91+
? "SingleAccount" : "MultipleAccount"));
92+
Log.w(TAG, "powerOptCheckEnabled forced to: " +
93+
application.getConfiguration().isPowerOptCheckForEnabled());
94+
loadAccountsAndAcquire(application, scopeString, pendingResult);
95+
}
96+
97+
@Override
98+
public void onError(MsalException exception) {
99+
Log.e(TAG, "Failed to create PCA: " + exception.getMessage(), exception);
100+
pendingResult.finish();
101+
}
102+
});
103+
}
104+
105+
private void loadAccountsAndAcquire(
106+
final IPublicClientApplication app,
107+
final String scopeString,
108+
final PendingResult pendingResult) {
109+
110+
if (app instanceof ISingleAccountPublicClientApplication) {
111+
final ISingleAccountPublicClientApplication singleApp =
112+
(ISingleAccountPublicClientApplication) app;
113+
try {
114+
final IAccount account = singleApp.getCurrentAccount().getCurrentAccount();
115+
if (account == null) {
116+
Log.e(TAG, "No signed-in account found in single-account mode.");
117+
pendingResult.finish();
118+
return;
119+
}
120+
doSilentAuth(app, account, scopeString, pendingResult);
121+
} catch (Exception e) {
122+
Log.e(TAG, "Error loading account: " + e.getMessage(), e);
123+
pendingResult.finish();
124+
}
125+
} else if (app instanceof IMultipleAccountPublicClientApplication) {
126+
final IMultipleAccountPublicClientApplication multiApp =
127+
(IMultipleAccountPublicClientApplication) app;
128+
multiApp.getAccounts(new IPublicClientApplication.LoadAccountsCallback() {
129+
@Override
130+
public void onTaskCompleted(List<IAccount> result) {
131+
if (result == null || result.isEmpty()) {
132+
Log.e(TAG, "No accounts found in multiple-account mode.");
133+
pendingResult.finish();
134+
return;
135+
}
136+
Log.w(TAG, "Found " + result.size() + " account(s). Using first.");
137+
doSilentAuth(app, result.get(0), scopeString, pendingResult);
138+
}
139+
140+
@Override
141+
public void onError(MsalException exception) {
142+
Log.e(TAG, "Error loading accounts: " + exception.getMessage(), exception);
143+
pendingResult.finish();
144+
}
145+
});
146+
}
147+
}
148+
149+
private void doSilentAuth(
150+
final IPublicClientApplication app,
151+
final IAccount account,
152+
final String scopeString,
153+
final PendingResult pendingResult) {
154+
155+
Log.w(TAG, "Calling acquireTokenSilent for account: " + account.getUsername());
156+
Log.w(TAG, "Authority: " + account.getAuthority());
157+
158+
final AcquireTokenSilentParameters parameters = new AcquireTokenSilentParameters.Builder()
159+
.forAccount(account)
160+
.fromAuthority(account.getAuthority())
161+
.withScopes(Arrays.asList(scopeString.toLowerCase().split(" ")))
162+
.forceRefresh(true) // Force network call to eSTS (no cache)
163+
.withCallback(new AuthenticationCallback() {
164+
@Override
165+
public void onSuccess(IAuthenticationResult authenticationResult) {
166+
Log.w(TAG, "=== SUCCESS === Token acquired silently!");
167+
Log.w(TAG, "Access token (first 20 chars): " +
168+
authenticationResult.getAccessToken().substring(0, 20) + "...");
169+
pendingResult.finish();
170+
}
171+
172+
@Override
173+
public void onError(MsalException exception) {
174+
Log.e(TAG, "=== FAILED === " + exception.getClass().getSimpleName());
175+
Log.e(TAG, "Error code: " + exception.getErrorCode());
176+
Log.e(TAG, "Message: " + exception.getMessage());
177+
if (exception.getCause() != null) {
178+
Log.e(TAG, "Cause: " + exception.getCause().getClass().getSimpleName()
179+
+ " - " + exception.getCause().getMessage());
180+
}
181+
pendingResult.finish();
182+
}
183+
184+
@Override
185+
public void onCancel() {
186+
Log.w(TAG, "=== CANCELLED ===");
187+
pendingResult.finish();
188+
}
189+
})
190+
.build();
191+
192+
app.acquireTokenSilentAsync(parameters);
193+
}
194+
}

0 commit comments

Comments
 (0)