Skip to content

Commit 9a67d3b

Browse files
rpdomeCopilot
andcommitted
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent a6d2373 commit 9a67d3b

3 files changed

Lines changed: 257 additions & 2 deletions

File tree

testapps/testapp/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
3939
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
4040
<uses-permission android:name="android.permission.REORDER_TASKS" />
41+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
4142

4243

4344
<application
@@ -96,6 +97,13 @@
9697
<action android:name="com.microsoft.identity.client.testapp.SILENT_AUTH" />
9798
</intent-filter>
9899
</receiver>
100+
101+
<!-- Foreground Service for testing silent auth at PROCESS_STATE_FOREGROUND_SERVICE
102+
priority (simulates FCM's FirebaseMessagingService). Compare results
103+
against SilentAuthReceiver to test Doze dozable-allow propagation. -->
104+
<service
105+
android:name="com.microsoft.identity.client.testapp.SilentAuthService"
106+
android:exported="true" />
99107
</application>
100108

101109
</manifest>

testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/SilentAuthReceiver.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,21 @@ public class SilentAuthReceiver extends BroadcastReceiver {
6262

6363
@Override
6464
public void onReceive(final Context context, final Intent intent) {
65+
if (!ACTION_SILENT_AUTH.equals(intent.getAction())) {
66+
Log.w(TAG, "Ignoring broadcast with unexpected action: " + intent.getAction());
67+
return;
68+
}
69+
6570
Log.w(TAG, "=== SilentAuthReceiver triggered (PROCESS_STATE_RECEIVER) ===");
6671

6772
final String scopes = intent.getStringExtra("scopes");
6873
final String scopeString = (scopes != null) ? scopes : "https://graph.microsoft.com/.default";
6974

7075
Log.w(TAG, "Scopes: " + scopeString);
7176

72-
// Use goAsync() to extend the receiver's lifecycle beyond the 10s limit
77+
// Use goAsync() so work can continue after onReceive() returns.
78+
// The broadcast still has a finite system time budget, so
79+
// PendingResult.finish() must be called promptly.
7380
final PendingResult pendingResult = goAsync();
7481

7582
// Create PCA with default config (uses broker)
@@ -143,6 +150,10 @@ public void onError(MsalException exception) {
143150
pendingResult.finish();
144151
}
145152
});
153+
} else {
154+
Log.e(TAG, "Unexpected app type: " + app.getClass().getName()
155+
+ " — neither single nor multiple account mode.");
156+
pendingResult.finish();
146157
}
147158
}
148159

@@ -158,7 +169,7 @@ private void doSilentAuth(
158169
final AcquireTokenSilentParameters parameters = new AcquireTokenSilentParameters.Builder()
159170
.forAccount(account)
160171
.fromAuthority(account.getAuthority())
161-
.withScopes(Arrays.asList(scopeString.toLowerCase().split(" ")))
172+
.withScopes(Arrays.asList(scopeString.trim().split("\\s+")))
162173
.forceRefresh(true) // Force network call to eSTS (no cache)
163174
.withCallback(new AuthenticationCallback() {
164175
@Override
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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.app.Notification;
26+
import android.app.NotificationChannel;
27+
import android.app.NotificationManager;
28+
import android.app.Service;
29+
import android.content.Intent;
30+
import android.os.Build;
31+
import android.os.IBinder;
32+
import android.util.Log;
33+
34+
import androidx.annotation.Nullable;
35+
36+
import com.microsoft.identity.client.AcquireTokenSilentParameters;
37+
import com.microsoft.identity.client.AuthenticationCallback;
38+
import com.microsoft.identity.client.IAccount;
39+
import com.microsoft.identity.client.IAuthenticationResult;
40+
import com.microsoft.identity.client.IMultipleAccountPublicClientApplication;
41+
import com.microsoft.identity.client.IPublicClientApplication;
42+
import com.microsoft.identity.client.ISingleAccountPublicClientApplication;
43+
import com.microsoft.identity.client.PublicClientApplication;
44+
import com.microsoft.identity.client.exception.MsalException;
45+
46+
import java.util.Arrays;
47+
import java.util.List;
48+
49+
/**
50+
* Foreground Service that triggers acquireTokenSilent from a background context.
51+
*
52+
* This runs at PROCESS_STATE_FOREGROUND_SERVICE priority, which is the same
53+
* priority as FCM's FirebaseMessagingService. This lets us test whether a
54+
* foreground Service caller propagates the dozable-allow firewall rule to the
55+
* Broker process during Doze mode, matching real-world FCM notification flows.
56+
*
57+
* Compare results against {@link SilentAuthReceiver} (PROCESS_STATE_RECEIVER)
58+
* to determine whether caller process importance affects Broker network access.
59+
*
60+
* Usage:
61+
* adb shell am start-foreground-service \
62+
* -n com.msft.identity.client.sample.local/com.microsoft.identity.client.testapp.SilentAuthService \
63+
* --es scopes "https://graph.microsoft.com/.default"
64+
*/
65+
public class SilentAuthService extends Service {
66+
67+
private static final String TAG = "SilentAuthService";
68+
private static final String CHANNEL_ID = "silent_auth_channel";
69+
private static final int NOTIFICATION_ID = 1001;
70+
71+
@Override
72+
public void onCreate() {
73+
super.onCreate();
74+
createNotificationChannel();
75+
}
76+
77+
@Override
78+
public int onStartCommand(final Intent intent, int flags, int startId) {
79+
Log.w(TAG, "=== SilentAuthService started (PROCESS_STATE_FOREGROUND_SERVICE) ===");
80+
81+
// Start as foreground immediately to avoid ANR and to elevate process state.
82+
final Notification notification = new Notification.Builder(this, CHANNEL_ID)
83+
.setContentTitle("Silent Auth Test")
84+
.setContentText("Running silent token acquisition...")
85+
.setSmallIcon(android.R.drawable.ic_dialog_info)
86+
.build();
87+
startForeground(NOTIFICATION_ID, notification);
88+
89+
final String scopes = (intent != null) ? intent.getStringExtra("scopes") : null;
90+
final String scopeString = (scopes != null) ? scopes : "https://graph.microsoft.com/.default";
91+
92+
Log.w(TAG, "Scopes: " + scopeString);
93+
94+
final int configResourceId = Constants.getResourceIdFromConfigFile(Constants.ConfigFile.DEFAULT);
95+
96+
PublicClientApplication.create(getApplicationContext(),
97+
configResourceId,
98+
new PublicClientApplication.ApplicationCreatedListener() {
99+
@Override
100+
public void onCreated(IPublicClientApplication application) {
101+
application.getConfiguration().setPowerOptCheckEnabled(false);
102+
103+
Log.w(TAG, "PCA created, mode: " +
104+
(application instanceof ISingleAccountPublicClientApplication
105+
? "SingleAccount" : "MultipleAccount"));
106+
Log.w(TAG, "powerOptCheckEnabled forced to: " +
107+
application.getConfiguration().isPowerOptCheckForEnabled());
108+
loadAccountsAndAcquire(application, scopeString);
109+
}
110+
111+
@Override
112+
public void onError(MsalException exception) {
113+
Log.e(TAG, "Failed to create PCA: " + exception.getMessage(), exception);
114+
finish();
115+
}
116+
});
117+
118+
return START_NOT_STICKY;
119+
}
120+
121+
@Nullable
122+
@Override
123+
public IBinder onBind(Intent intent) {
124+
return null;
125+
}
126+
127+
private void loadAccountsAndAcquire(
128+
final IPublicClientApplication app,
129+
final String scopeString) {
130+
131+
if (app instanceof ISingleAccountPublicClientApplication) {
132+
final ISingleAccountPublicClientApplication singleApp =
133+
(ISingleAccountPublicClientApplication) app;
134+
try {
135+
final IAccount account = singleApp.getCurrentAccount().getCurrentAccount();
136+
if (account == null) {
137+
Log.e(TAG, "No signed-in account found in single-account mode.");
138+
finish();
139+
return;
140+
}
141+
doSilentAuth(app, account, scopeString);
142+
} catch (Exception e) {
143+
Log.e(TAG, "Error loading account: " + e.getMessage(), e);
144+
finish();
145+
}
146+
} else if (app instanceof IMultipleAccountPublicClientApplication) {
147+
final IMultipleAccountPublicClientApplication multiApp =
148+
(IMultipleAccountPublicClientApplication) app;
149+
multiApp.getAccounts(new IPublicClientApplication.LoadAccountsCallback() {
150+
@Override
151+
public void onTaskCompleted(List<IAccount> result) {
152+
if (result == null || result.isEmpty()) {
153+
Log.e(TAG, "No accounts found in multiple-account mode.");
154+
finish();
155+
return;
156+
}
157+
Log.w(TAG, "Found " + result.size() + " account(s). Using first.");
158+
doSilentAuth(app, result.get(0), scopeString);
159+
}
160+
161+
@Override
162+
public void onError(MsalException exception) {
163+
Log.e(TAG, "Error loading accounts: " + exception.getMessage(), exception);
164+
finish();
165+
}
166+
});
167+
} else {
168+
Log.e(TAG, "Unexpected app type: " + app.getClass().getName()
169+
+ " — neither single nor multiple account mode.");
170+
finish();
171+
}
172+
}
173+
174+
private void doSilentAuth(
175+
final IPublicClientApplication app,
176+
final IAccount account,
177+
final String scopeString) {
178+
179+
Log.w(TAG, "Calling acquireTokenSilent for account: " + account.getUsername());
180+
Log.w(TAG, "Authority: " + account.getAuthority());
181+
182+
final AcquireTokenSilentParameters parameters = new AcquireTokenSilentParameters.Builder()
183+
.forAccount(account)
184+
.fromAuthority(account.getAuthority())
185+
.withScopes(Arrays.asList(scopeString.trim().split("\\s+")))
186+
.forceRefresh(true) // Force network call to eSTS (no cache)
187+
.withCallback(new AuthenticationCallback() {
188+
@Override
189+
public void onSuccess(IAuthenticationResult authenticationResult) {
190+
Log.w(TAG, "=== SUCCESS === Token acquired silently!");
191+
Log.w(TAG, "Silent token acquisition completed successfully.");
192+
finish();
193+
}
194+
195+
@Override
196+
public void onError(MsalException exception) {
197+
Log.e(TAG, "=== FAILED === " + exception.getClass().getSimpleName());
198+
Log.e(TAG, "Error code: " + exception.getErrorCode());
199+
Log.e(TAG, "Message: " + exception.getMessage());
200+
if (exception.getCause() != null) {
201+
Log.e(TAG, "Cause: " + exception.getCause().getClass().getSimpleName()
202+
+ " - " + exception.getCause().getMessage());
203+
}
204+
finish();
205+
}
206+
207+
@Override
208+
public void onCancel() {
209+
Log.w(TAG, "=== CANCELLED ===");
210+
finish();
211+
}
212+
})
213+
.build();
214+
215+
app.acquireTokenSilentAsync(parameters);
216+
}
217+
218+
private void finish() {
219+
stopForeground(true);
220+
stopSelf();
221+
}
222+
223+
private void createNotificationChannel() {
224+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
225+
final NotificationChannel channel = new NotificationChannel(
226+
CHANNEL_ID,
227+
"Silent Auth Test",
228+
NotificationManager.IMPORTANCE_LOW);
229+
channel.setDescription("Notification channel for silent auth Doze test");
230+
final NotificationManager manager = getSystemService(NotificationManager.class);
231+
if (manager != null) {
232+
manager.createNotificationChannel(channel);
233+
}
234+
}
235+
}
236+
}

0 commit comments

Comments
 (0)