Skip to content

Commit c207fcc

Browse files
authored
Native auth safe logging test (#2092)
### Comments and Changes Summary: - Setup external logger and remove it for every test under `@Before ` and `@After`. - Use Regex rules to filter the log message under `verify().log()`. There are two pools for the regex rules: `sensitivePIIMessages` - items should not exist in the log regardless of allowPII value; `permittedPIIMessages `- items should not exist in the log when `allowPII=false` but can exist when `allowPII=true` - Use `@ParameterizedRobolectricTestRunner.Parameters` in order to test both `allow=false & allowPII=true` for `RunWith(ParameterizedRobolectricTestRunner::class) NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean)` - `verifyLogDoesNotContain` checks `message ` and 'containsPII'. `verifyLogDoesNotContain ` checks `message`.
1 parent fec0f48 commit c207fcc

4 files changed

Lines changed: 161 additions & 20 deletions

File tree

msal/src/main/java/com/microsoft/identity/client/Logger.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ public void log(String tag, com.microsoft.identity.common.internal.logging.Logge
175175
mExternalLogger = externalLogger;
176176
}
177177

178+
public synchronized void removeExternalLogger() {
179+
mExternalLogger = null;
180+
}
181+
182+
178183
/**
179184
* Enable/Disable the Android logcat logging. By default, the sdk enables it.
180185
*

msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationJavaTest.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import android.app.Activity;
2828
import android.content.Context;
2929

30+
import com.microsoft.identity.client.ILoggerCallback;
3031
import com.microsoft.identity.client.Logger;
3132
import com.microsoft.identity.client.PublicClientApplication;
3233
import com.microsoft.identity.client.e2e.shadows.ShadowAndroidSdkStorageEncryptionManager;
@@ -74,6 +75,7 @@
7475
import com.microsoft.identity.common.java.util.ResultFuture;
7576
import com.microsoft.identity.internal.testutils.TestUtils;
7677
import com.microsoft.identity.nativeauth.statemachine.states.SignUpPasswordRequiredState;
78+
import com.microsoft.identity.nativeauth.utils.LoggerCheckHelper;
7779

7880
import org.junit.After;
7981
import org.junit.AfterClass;
@@ -82,13 +84,17 @@
8284
import org.junit.Ignore;
8385
import org.junit.Test;
8486
import org.junit.runner.RunWith;
87+
import org.mockito.Mock;
8588
import org.mockito.Mockito;
86-
import org.robolectric.RobolectricTestRunner;
89+
import org.mockito.MockitoAnnotations;
90+
import org.robolectric.ParameterizedRobolectricTestRunner;
8791
import org.robolectric.annotation.Config;
8892
import org.robolectric.annotation.LooperMode;
8993

9094
import java.io.File;
9195
import java.lang.reflect.InvocationTargetException;
96+
import java.util.Arrays;
97+
import java.util.Collection;
9298
import java.util.UUID;
9399
import java.util.concurrent.ExecutionException;
94100
import java.util.concurrent.TimeUnit;
@@ -106,7 +112,7 @@
106112
import static org.mockito.Mockito.spy;
107113
import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
108114

109-
@RunWith(RobolectricTestRunner.class)
115+
@RunWith(ParameterizedRobolectricTestRunner.class)
110116
@LooperMode(LEGACY)
111117
@Config(shadows = {ShadowAndroidSdkStorageEncryptionManager.class})
112118
public class NativeAuthPublicClientApplicationJavaTest extends PublicClientApplicationAbstractTest {
@@ -115,11 +121,20 @@ public class NativeAuthPublicClientApplicationJavaTest extends PublicClientAppli
115121
private IPlatformComponents components;
116122
private Activity activity;
117123
private INativeAuthPublicClientApplication application;
124+
private LoggerCheckHelper loggerCheckHelper;
118125
private final String username = "user@email.com";
119126
private final String invalidUsername = "invalidUsername";
120127
private final char[] password = "verySafePassword".toCharArray();
121128
private final String code = "1234";
122129
private final String emptyString = "";
130+
private final boolean allowPII;
131+
132+
public NativeAuthPublicClientApplicationJavaTest(boolean allowPII) {
133+
this.allowPII = allowPII;
134+
}
135+
136+
@Mock
137+
private ILoggerCallback externalLogger;
123138

124139

125140
@Override
@@ -137,21 +152,26 @@ public static void tearDownClass() {
137152
setUseMockApiForNativeAuth(false);
138153
}
139154

155+
@ParameterizedRobolectricTestRunner.Parameters
156+
public static Collection<Boolean> data() {
157+
return Arrays.asList(true, false);
158+
}
159+
140160
@Before
141161
public void setup() {
162+
MockitoAnnotations.initMocks(this);
142163
context = ApplicationProvider.getApplicationContext();
143164
components = AndroidPlatformComponentsFactory.createFromContext(context);
144165
activity = Mockito.mock(Activity.class);
166+
loggerCheckHelper = new LoggerCheckHelper(externalLogger, allowPII);
145167
Mockito.when(activity.getApplicationContext()).thenReturn(context);
146168
setupPCA();
147-
Logger.getInstance().setEnableLogcatLog(true);
148-
Logger.getInstance().setEnablePII(true);
149-
Logger.getInstance().setLogLevel(Logger.LogLevel.VERBOSE);
150169
CommandDispatcherHelper.clear();
151170
}
152171

153172
@After
154173
public void cleanup() {
174+
loggerCheckHelper.checkSafeLogging();
155175
AcquireTokenTestHelper.setAccount(null);
156176
// remove everything from cache after test ends
157177
TestUtils.clearCache(SHARED_PREFERENCES_NAME);

msal/src/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,29 @@ package com.microsoft.identity.nativeauth
2525
import android.app.Activity
2626
import android.content.Context
2727
import androidx.test.core.app.ApplicationProvider
28+
import com.microsoft.identity.client.ILoggerCallback
29+
import com.microsoft.identity.client.Logger
2830
import com.microsoft.identity.client.PublicClientApplication
2931
import com.microsoft.identity.client.e2e.shadows.ShadowAndroidSdkStorageEncryptionManager
3032
import com.microsoft.identity.client.e2e.tests.PublicClientApplicationAbstractTest
3133
import com.microsoft.identity.client.e2e.utils.AcquireTokenTestHelper
3234
import com.microsoft.identity.client.exception.MsalClientException
3335
import com.microsoft.identity.client.exception.MsalException
36+
import com.microsoft.identity.common.components.AndroidPlatformComponentsFactory
37+
import com.microsoft.identity.common.internal.controllers.CommandDispatcherHelper
38+
import com.microsoft.identity.common.java.exception.BaseException
39+
import com.microsoft.identity.common.java.interfaces.IPlatformComponents
40+
import com.microsoft.identity.common.java.nativeauth.BuildValues
41+
import com.microsoft.identity.common.java.util.ResultFuture
42+
import com.microsoft.identity.common.nativeauth.MockApiEndpoint
43+
import com.microsoft.identity.common.nativeauth.MockApiResponseType
44+
import com.microsoft.identity.common.nativeauth.MockApiUtils.Companion.configureMockApi
45+
import com.microsoft.identity.internal.testutils.TestUtils
46+
import com.microsoft.identity.nativeauth.statemachine.errors.ErrorTypes
3447
import com.microsoft.identity.nativeauth.statemachine.errors.GetAccessTokenError
3548
import com.microsoft.identity.nativeauth.statemachine.errors.ResetPasswordError
3649
import com.microsoft.identity.nativeauth.statemachine.errors.ResetPasswordSubmitPasswordError
50+
import com.microsoft.identity.nativeauth.statemachine.errors.SignInContinuationError
3751
import com.microsoft.identity.nativeauth.statemachine.errors.SignInError
3852
import com.microsoft.identity.nativeauth.statemachine.errors.SignUpError
3953
import com.microsoft.identity.nativeauth.statemachine.errors.SignUpSubmitAttributesError
@@ -48,20 +62,9 @@ import com.microsoft.identity.nativeauth.statemachine.results.SignInResult
4862
import com.microsoft.identity.nativeauth.statemachine.results.SignOutResult
4963
import com.microsoft.identity.nativeauth.statemachine.results.SignUpResendCodeResult
5064
import com.microsoft.identity.nativeauth.statemachine.results.SignUpResult
51-
import com.microsoft.identity.common.components.AndroidPlatformComponentsFactory
52-
import com.microsoft.identity.common.internal.controllers.CommandDispatcherHelper
53-
import com.microsoft.identity.common.nativeauth.MockApiEndpoint
54-
import com.microsoft.identity.common.nativeauth.MockApiResponseType
55-
import com.microsoft.identity.common.nativeauth.MockApiUtils.Companion.configureMockApi
56-
import com.microsoft.identity.common.java.exception.BaseException
57-
import com.microsoft.identity.common.java.interfaces.IPlatformComponents
58-
import com.microsoft.identity.common.java.nativeauth.BuildValues
59-
import com.microsoft.identity.common.java.util.ResultFuture
60-
import com.microsoft.identity.internal.testutils.TestUtils
61-
import com.microsoft.identity.nativeauth.statemachine.errors.ErrorTypes
6265
import com.microsoft.identity.nativeauth.statemachine.states.SignInContinuationState
66+
import com.microsoft.identity.nativeauth.utils.LoggerCheckHelper
6367
import com.microsoft.identity.nativeauth.utils.mockCorrelationId
64-
import com.microsoft.identity.nativeauth.statemachine.errors.SignInContinuationError
6568
import kotlinx.coroutines.ExperimentalCoroutinesApi
6669
import kotlinx.coroutines.runBlocking
6770
import kotlinx.coroutines.test.runTest
@@ -76,35 +79,55 @@ import org.junit.BeforeClass
7679
import org.junit.Ignore
7780
import org.junit.Test
7881
import org.junit.runner.RunWith
82+
import org.mockito.ArgumentMatcher
83+
import org.mockito.ArgumentMatchers.anyBoolean
84+
import org.mockito.Mock
7985
import org.mockito.Mockito
86+
import org.mockito.MockitoAnnotations
87+
import org.mockito.kotlin.any
88+
import org.mockito.kotlin.argThat
89+
import org.mockito.kotlin.eq
8090
import org.mockito.kotlin.mock
91+
import org.mockito.kotlin.never
8192
import org.mockito.kotlin.spy
93+
import org.mockito.kotlin.verify
8294
import org.mockito.kotlin.whenever
83-
import org.robolectric.RobolectricTestRunner
95+
import org.robolectric.ParameterizedRobolectricTestRunner
8496
import org.robolectric.annotation.Config
8597
import java.io.File
8698
import java.util.UUID
8799
import java.util.concurrent.ExecutionException
88100
import java.util.concurrent.TimeUnit
89101
import java.util.concurrent.TimeoutException
90102

103+
91104
@ExperimentalCoroutinesApi
92-
@RunWith(RobolectricTestRunner::class)
105+
@RunWith(ParameterizedRobolectricTestRunner::class)
93106
@Config(shadows = [ShadowAndroidSdkStorageEncryptionManager::class])
94-
class NativeAuthPublicClientApplicationKotlinTest : PublicClientApplicationAbstractTest() {
107+
class NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean) : PublicClientApplicationAbstractTest() {
95108
private lateinit var context: Context
96109
private lateinit var components: IPlatformComponents
97110
private lateinit var activity: Activity
98111
private lateinit var application: INativeAuthPublicClientApplication
112+
private lateinit var loggerCheckHelper: LoggerCheckHelper
99113
private val username = "user@email.com"
100114
private val invalidUsername = "invalidUsername"
101115
private val password = "verySafePassword".toCharArray()
102116
private val code = "1234"
103117
private val emptyString = ""
104118

119+
@Mock
120+
private lateinit var externalLogger: ILoggerCallback
121+
105122
override fun getConfigFilePath() = "src/test/res/raw/native_auth_native_only_test_config.json"
106123

107124
companion object {
125+
@JvmStatic
126+
@ParameterizedRobolectricTestRunner.Parameters
127+
fun data(): Collection<Boolean> {
128+
return listOf(true, false)
129+
}
130+
108131
@BeforeClass
109132
@JvmStatic
110133
fun setupClass() {
@@ -120,16 +143,19 @@ class NativeAuthPublicClientApplicationKotlinTest : PublicClientApplicationAbstr
120143

121144
@Before
122145
override fun setup() {
146+
MockitoAnnotations.initMocks(this)
123147
context = ApplicationProvider.getApplicationContext()
124148
components = AndroidPlatformComponentsFactory.createFromContext(context)
125149
activity = Mockito.mock(Activity::class.java)
150+
loggerCheckHelper = LoggerCheckHelper(externalLogger, allowPII)
126151
whenever(activity.applicationContext).thenReturn(context)
127152
setupPCA()
128153
CommandDispatcherHelper.clear()
129154
}
130155

131156
@After
132157
fun cleanup() {
158+
loggerCheckHelper.checkSafeLogging()
133159
AcquireTokenTestHelper.setAccount(null)
134160
// remove everything from cache after test ends
135161
TestUtils.clearCache(SHARED_PREFERENCES_NAME)
@@ -424,6 +450,7 @@ class NativeAuthPublicClientApplicationKotlinTest : PublicClientApplicationAbstr
424450
)
425451
val result = continuationTokenState.signIn(scopes = null)
426452
assertTrue(result is SignInContinuationError)
453+
427454
}
428455

429456
/**
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.microsoft.identity.nativeauth.utils
2+
3+
import com.microsoft.identity.client.ILoggerCallback
4+
import com.microsoft.identity.client.Logger
5+
import org.mockito.ArgumentMatcher
6+
import org.mockito.ArgumentMatchers
7+
import org.mockito.kotlin.any
8+
import org.mockito.kotlin.argThat
9+
import org.mockito.kotlin.eq
10+
import org.mockito.kotlin.never
11+
import org.mockito.kotlin.verify
12+
13+
class LoggerCheckHelper(private val externalLogger: ILoggerCallback, private val allowPII: Boolean) {
14+
15+
private val sensitivePIIMessages = listOf(
16+
"""(?<![\[\(])["]password["][:=]?(?![\]\)\}])""", // '"password":' '"password"=' exclude 'password' '"challengeType":["password"]' '"challenge_type":"password"}'
17+
"""(?<![\s\?\(])(code)[:=]""", // 'code:' 'code=' exclude 'codeLength' 'error?code'
18+
"""(?<![\(])continuationToken[:=]""",
19+
"""(?<![\(])attributes[:=]""",
20+
"""(?i)\b(accessToken|access_token)[:=]""", // access_token, accessToken
21+
"""(?i)\b(refreshToken|refresh_token)[:=]""",
22+
"""(?i)\b(idToken|id_token)[:=]""",
23+
"""(?i)\b(continuation_token)[:=]""",
24+
"""^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$""" // JWT token
25+
)
26+
private val permittedPIIMessages = listOf(
27+
"""(?<![\(])username[:=]""",
28+
"""(?i)\b(challengeTargetLabel|challenge_target_label)[:=]"""
29+
)
30+
31+
init {
32+
setupLogger()
33+
}
34+
35+
private fun setupLogger() {
36+
Logger.getInstance().setLogLevel(Logger.LogLevel.INFO)
37+
Logger.getInstance().setEnablePII(allowPII)
38+
Logger.getInstance().setExternalLogger(externalLogger)
39+
}
40+
41+
private fun clearLogger() {
42+
Logger.getInstance().removeExternalLogger()
43+
}
44+
45+
fun checkSafeLogging() {
46+
var allowList = listOf<String>()
47+
val disableList: List<String>
48+
49+
if (allowPII) {
50+
allowList = permittedPIIMessages
51+
disableList = sensitivePIIMessages
52+
} else {
53+
disableList = sensitivePIIMessages + permittedPIIMessages
54+
}
55+
56+
allowList.forEach { regex ->
57+
verifyLogCouldContain(regex)
58+
}
59+
disableList.forEach { regex ->
60+
verifyLogDoesNotContain(regex)
61+
}
62+
63+
clearLogger()
64+
}
65+
66+
private fun verifyLogDoesNotContain(regex: String) {
67+
verify(externalLogger, never()).log(
68+
any(),
69+
any(),
70+
argThat(RegexMatcher(regex)),
71+
ArgumentMatchers.anyBoolean()
72+
)
73+
}
74+
75+
private fun verifyLogCouldContain(regex: String) {
76+
verify(externalLogger, never()).log(
77+
any(),
78+
any(),
79+
argThat(RegexMatcher(regex)), // allowList items are logged but the containsPII should be true.
80+
eq(false)
81+
)
82+
}
83+
84+
class RegexMatcher(private val regex: String) : ArgumentMatcher<String> {
85+
override fun matches(argument: String?): Boolean {
86+
return regex.toRegex().containsMatchIn(argument ?: "")
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)