Skip to content

Commit f357998

Browse files
authored
Support for multiple tokens in native auth flows (#2082)
This PR and its companion PR (AzureAD/microsoft-authentication-library-common-for-android#2390) enables multiple access token support for Native Auth. A pair of new method have been added in the AccountState class to allow developer to retrieve different access tokens according to the requested scopes. The way these methods acquire tokens is exactly as same as the one in MSAL. The old method 'getAccessToken''s functionality hasn't been changed. It still returns the access token matching the default OIDC scope. This method though has been deprecated.
1 parent 1281496 commit f357998

8 files changed

Lines changed: 443 additions & 68 deletions

File tree

changelog

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ MSAL Wiki : https://github.com/AzureAD/microsoft-authentication-library-for-andr
22

33
vNext
44
----------
5+
- [MINOR] Support for multiple access tokens in NativeAuth (#2082)
56

67
Version 5.3.1
78
---------

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public ClaimsRequest getClaimsRequest() {
136136
return mClaimsRequest;
137137
}
138138

139-
void setAccountRecord(AccountRecord record) {
139+
public void setAccountRecord(AccountRecord record) {
140140
mAccountRecord = record;
141141
}
142142

msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
import com.microsoft.identity.common.logging.Logger;
6969
import com.microsoft.identity.common.java.nativeauth.authorities.NativeAuthCIAMAuthority;
7070
import com.microsoft.identity.common.java.nativeauth.commands.parameters.SignInWithContinuationTokenCommandParameters;
71-
import com.microsoft.identity.common.java.nativeauth.commands.parameters.AcquireTokenNoFixedScopesCommandParameters;
7271
import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordResendCodeCommandParameters;
7372
import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordStartCommandParameters;
7473
import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordSubmitCodeCommandParameters;
@@ -246,9 +245,9 @@ public static SilentTokenCommandParameters createSilentTokenCommandParameters(
246245
.forceRefresh(forceRefresh)
247246
.account(parameters.getAccountRecord())
248247
.authenticationScheme(authenticationScheme)
249-
.scopes(new HashSet<>(parameters.getScopes()))
250248
.powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled())
251249
.correlationId(parameters.getCorrelationId())
250+
.scopes(new HashSet<>(parameters.getScopes()))
252251
.build();
253252

254253
return commandParameters;
@@ -294,50 +293,6 @@ public static DeviceCodeFlowCommandParameters createDeviceCodeFlowWithClaimsComm
294293
return commandParameters;
295294
}
296295

297-
/**
298-
* Creates command parameter for [{@link com.microsoft.identity.common.nativeauth.internal.commands.AcquireTokenNoFixedScopesCommand}] of Native Auth.
299-
*
300-
* @param configuration PCA configuration
301-
* @param tokenCache token cache for storing results
302-
* @param accountRecord accountRecord object containing account information
303-
* @param forceRefresh boolean parameter to denote if refresh should be forced
304-
* @param correlationId correlation ID to use in the API request, taken from the previous API response in the flow
305-
* @return Command parameter object
306-
* @throws ClientException
307-
*/
308-
public static AcquireTokenNoFixedScopesCommandParameters createAcquireTokenNoFixedScopesCommandParameters(
309-
@NonNull final PublicClientApplicationConfiguration configuration,
310-
@NonNull final OAuth2TokenCache tokenCache,
311-
@NonNull final AccountRecord accountRecord,
312-
@NonNull final Boolean forceRefresh,
313-
@NonNull final String correlationId) throws ClientException {
314-
final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority());
315-
316-
final AbstractAuthenticationScheme authenticationScheme = new BearerAuthenticationSchemeInternal();
317-
318-
final AcquireTokenNoFixedScopesCommandParameters commandParameters = AcquireTokenNoFixedScopesCommandParameters
319-
.builder()
320-
.platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()))
321-
.applicationName(configuration.getAppContext().getPackageName())
322-
.applicationVersion(getPackageVersion(configuration.getAppContext()))
323-
.clientId(configuration.getClientId())
324-
.isSharedDevice(configuration.getIsSharedDevice())
325-
.oAuth2TokenCache(tokenCache)
326-
.redirectUri(configuration.getRedirectUri())
327-
.requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion())
328-
.sdkType(SdkType.MSAL)
329-
.sdkVersion(PublicClientApplication.getSdkVersion())
330-
.authority(authority)
331-
.authenticationScheme(authenticationScheme)
332-
.forceRefresh(forceRefresh)
333-
.account(accountRecord)
334-
.correlationId(correlationId)
335-
.powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled())
336-
.build();
337-
338-
return commandParameters;
339-
}
340-
341296
public static DeviceCodeFlowCommandParameters createDeviceCodeFlowCommandParameters(
342297
@NonNull final PublicClientApplicationConfiguration configuration,
343298
@NonNull final OAuth2TokenCache tokenCache,

msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/errors/GetAccessTokenError.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,17 @@ import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResu
2727

2828
internal class GetAccessTokenErrorTypes {
2929
companion object {
30-
const val NO_ACCOUNT_FOUND = "no_account_found"
30+
/*
31+
* The INVALID_SCOPES value indicates the scopes provided by the user are not valid
32+
* If this occurs, valid scopes should be resubmitted
33+
*/
34+
const val INVALID_SCOPES = "invalid_scopes"
35+
36+
/*
37+
* The NO_ACCOUNT_FOUND value indicates the user is not signed in.
38+
* If this occurs, the API should be called after successful sign in
39+
*/
40+
const val NO_ACCOUNT_FOUND = "invalid_scopes"
3141
}
3242
}
3343

@@ -40,4 +50,6 @@ class GetAccessTokenError(
4050
override var exception: Exception? = null
4151
): GetAccessTokenResult, Error(errorType = errorType, error = error, errorMessage= errorMessage, correlationId = correlationId, errorCodes = errorCodes, exception = exception) {
4252
fun isNoAccountFound() : Boolean = this.errorType == GetAccessTokenErrorTypes.NO_ACCOUNT_FOUND
53+
54+
fun isInvalidScopes(): Boolean = this.errorType == GetAccessTokenErrorTypes.INVALID_SCOPES
4355
}

msal/src/main/java/com/microsoft/identity/nativeauth/statemachine/states/AccountState.kt

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ import com.microsoft.identity.client.exception.MsalException
3636
import com.microsoft.identity.client.internal.CommandParametersAdapter
3737
import com.microsoft.identity.common.internal.commands.RemoveCurrentAccountCommand
3838
import com.microsoft.identity.common.internal.controllers.LocalMSALController
39+
import com.microsoft.identity.common.java.AuthenticationConstants
3940
import com.microsoft.identity.common.java.commands.CommandCallback
41+
import com.microsoft.identity.common.java.commands.SilentTokenCommand
4042
import com.microsoft.identity.common.java.controllers.CommandDispatcher
4143
import com.microsoft.identity.common.java.controllers.ExceptionAdapter
4244
import com.microsoft.identity.common.java.dto.AccountRecord
@@ -46,7 +48,7 @@ import com.microsoft.identity.common.java.exception.ServiceException
4648
import com.microsoft.identity.common.java.logging.LogSession
4749
import com.microsoft.identity.common.java.logging.Logger
4850
import com.microsoft.identity.common.java.result.ILocalAuthenticationResult
49-
import com.microsoft.identity.common.nativeauth.internal.commands.AcquireTokenNoFixedScopesCommand
51+
import com.microsoft.identity.common.java.result.LocalAuthenticationResult
5052
import com.microsoft.identity.common.nativeauth.internal.controllers.NativeAuthMsalController
5153
import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplication
5254
import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplicationConfiguration
@@ -60,6 +62,7 @@ import com.microsoft.identity.nativeauth.utils.serializable
6062
import kotlinx.coroutines.Dispatchers
6163
import kotlinx.coroutines.launch
6264
import kotlinx.coroutines.withContext
65+
import java.util.UUID
6366

6467
/**
6568
* AccountState returned as part of a successful completion of sign in flow [com.microsoft.identity.nativeauth.statemachine.results.SignInResult.Complete].
@@ -207,13 +210,11 @@ class AccountState private constructor(
207210
interface GetAccessTokenCallback : Callback<GetAccessTokenResult>
208211

209212
/**
210-
* Retrieves the access token for the currently signed in account from the cache.
213+
* Retrieves the access token for the default OIDC (openid, offline_access, profile) scopes from the cache
211214
* If the access token is expired, it will be attempted to be refreshed using the refresh token that's stored in the cache;
212215
* callback variant.
213216
*
214217
* @return [com.microsoft.identity.client.IAuthenticationResult] If successful.
215-
* @throws [MsalClientException] If the the account doesn't exist in the cache.
216-
* @throws [ServiceException] If the refresh token doesn't exist in the cache/is expired, or the refreshing fails.
217218
*/
218219
fun getAccessToken(forceRefresh: Boolean = false, callback: GetAccessTokenCallback) {
219220
LogSession.logMethodCall(
@@ -233,18 +234,70 @@ class AccountState private constructor(
233234
}
234235

235236
/**
236-
* Retrieves the access token for the currently signed in account from the cache.
237+
* Retrieves the access token for the default OIDC (openid, offline_access, profile) scopes from the cache.
237238
* If the access token is expired, it will be attempted to be refreshed using the refresh token that's stored in the cache;
238239
* Kotlin coroutines variant.
239240
*
240241
* @return [com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult] The result of the getAccessToken action
241242
*/
242243
suspend fun getAccessToken(forceRefresh: Boolean = false): GetAccessTokenResult {
244+
return getAccessTokenInternal(forceRefresh, AuthenticationConstants.DEFAULT_SCOPES.toList());
245+
}
246+
247+
/**
248+
* Retrieves the access token for the currently signed in account from the cache such that
249+
* the scope of retrieved access token is a superset of requested scopes. If the access token
250+
* has expired, it will be refreshed using the refresh token that's stored in the cache. If no
251+
* access token matching the requested scopes is found in cache then a new access token is fetched.
252+
* Kotlin coroutines variant.
253+
*
254+
* @return [com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult] The result of the getAccessToken action
255+
*/
256+
suspend fun getAccessToken(forceRefresh: Boolean = false, scopes: List<String>): GetAccessTokenResult {
257+
if (scopes.isEmpty()) {
258+
return GetAccessTokenError(
259+
errorType = GetAccessTokenErrorTypes.INVALID_SCOPES,
260+
errorMessage = "Empty or invalid scopes",
261+
correlationId = correlationId
262+
)
263+
}
264+
265+
return getAccessTokenInternal(forceRefresh, scopes)
266+
}
267+
268+
/**
269+
* Retrieves the access token for the currently signed in account from the cache such that
270+
* the scope of retrieved access token is a superset of requested scopes. If the access token
271+
* has expired, it will be refreshed using the refresh token that's stored in the cache. If no
272+
* access token matching the requested scopes is found in cache then a new access token is fetched.
273+
* callback variant.
274+
*
275+
* @return [com.microsoft.identity.client.IAuthenticationResult] If successful.
276+
*/
277+
fun getAccessToken(forceRefresh: Boolean = false, scopes: List<String>, callback: GetAccessTokenCallback) {
278+
LogSession.logMethodCall(
279+
tag = TAG,
280+
correlationId = null,
281+
methodName = "$TAG.getAccessToken"
282+
)
283+
NativeAuthPublicClientApplication.pcaScope.launch {
284+
try {
285+
val result = getAccessToken(forceRefresh, scopes)
286+
callback.onResult(result)
287+
} catch (e: MsalException) {
288+
Logger.error(TAG, "Exception thrown in getAccessToken", e)
289+
callback.onError(e)
290+
}
291+
}
292+
}
293+
294+
private suspend fun getAccessTokenInternal(forceRefresh: Boolean, scopes: List<String>): GetAccessTokenResult {
243295
LogSession.logMethodCall(
244296
tag = TAG,
245297
correlationId = null,
246298
methodName = "$TAG.getAccessToken(forceRefresh: Boolean)"
247299
)
300+
248301
return withContext(Dispatchers.IO) {
249302
try {
250303
val currentAccount =
@@ -253,31 +306,45 @@ class AccountState private constructor(
253306
errorType = GetAccessTokenErrorTypes.NO_ACCOUNT_FOUND,
254307
error = MsalClientException.NO_CURRENT_ACCOUNT,
255308
errorMessage = MsalClientException.NO_CURRENT_ACCOUNT_ERROR_MESSAGE,
256-
correlationId = "UNSET"
309+
correlationId = correlationId
257310
)
258311

259312
val acquireTokenSilentParameters = AcquireTokenSilentParameters.Builder()
260313
.forAccount(currentAccount)
261314
.fromAuthority(currentAccount.authority)
315+
.withCorrelationId(UUID.fromString(correlationId))
316+
.forceRefresh(forceRefresh)
317+
.withScopes(scopes)
262318
.build()
263319

264-
val accountToBeUsed = PublicClientApplication.selectAccountRecordForTokenRequest(
320+
val accountRecord = PublicClientApplication.selectAccountRecordForTokenRequest(
265321
config,
266322
acquireTokenSilentParameters
267323
)
324+
acquireTokenSilentParameters.accountRecord = accountRecord
268325

269-
val params =
270-
CommandParametersAdapter.createAcquireTokenNoFixedScopesCommandParameters(
271-
config,
272-
config.oAuth2TokenCache,
273-
accountToBeUsed,
274-
forceRefresh,
275-
correlationId
276-
)
326+
val params = CommandParametersAdapter.createSilentTokenCommandParameters(
327+
config,
328+
config.oAuth2TokenCache,
329+
acquireTokenSilentParameters
330+
)
277331

278-
val command = AcquireTokenNoFixedScopesCommand(
332+
val command = SilentTokenCommand(
279333
params,
280-
NativeAuthMsalController(),
334+
NativeAuthMsalController().asControllerFactory(),
335+
object : CommandCallback<LocalAuthenticationResult?, BaseException?> {
336+
override fun onError(error: BaseException?) {
337+
// Do nothing, handled by CommandDispatcher.submitSilentReturningFuture()
338+
}
339+
340+
override fun onTaskCompleted(result: LocalAuthenticationResult?) {
341+
// Do nothing, handled by CommandDispatcher.submitSilentReturningFuture()
342+
}
343+
344+
override fun onCancel() {
345+
// Do nothing
346+
}
347+
},
281348
PublicApiId.NATIVE_AUTH_ACCOUNT_GET_ACCESS_TOKEN
282349
)
283350

@@ -288,14 +355,14 @@ class AccountState private constructor(
288355
is ServiceException -> {
289356
GetAccessTokenError(
290357
exception = ExceptionAdapter.convertToNativeAuthException(commandResult),
291-
correlationId = "UNSET"
358+
correlationId = commandResult.correlationId ?: correlationId
292359
)
293360
}
294361

295362
is Exception -> {
296363
GetAccessTokenError(
297364
exception = commandResult,
298-
correlationId = "UNSET"
365+
correlationId = correlationId
299366
)
300367
}
301368

0 commit comments

Comments
 (0)