Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for multiple tokens in native auth flows #2082

Merged
merged 25 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ MSAL Wiki : https://github.com/AzureAD/microsoft-authentication-library-for-andr

vNext
----------
- [PATCH] Support for multiple access tokens in NativeAuth (#2082)
SammyO marked this conversation as resolved.
Show resolved Hide resolved

Version 5.3.1
---------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public ClaimsRequest getClaimsRequest() {
return mClaimsRequest;
}

void setAccountRecord(AccountRecord record) {
public void setAccountRecord(AccountRecord record) {
SammyO marked this conversation as resolved.
Show resolved Hide resolved
mAccountRecord = record;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@
import com.microsoft.identity.common.logging.Logger;
import com.microsoft.identity.common.java.nativeauth.authorities.NativeAuthCIAMAuthority;
import com.microsoft.identity.common.java.nativeauth.commands.parameters.SignInWithContinuationTokenCommandParameters;
import com.microsoft.identity.common.java.nativeauth.commands.parameters.AcquireTokenNoFixedScopesCommandParameters;
import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordResendCodeCommandParameters;
import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordStartCommandParameters;
import com.microsoft.identity.common.java.nativeauth.commands.parameters.ResetPasswordSubmitCodeCommandParameters;
Expand Down Expand Up @@ -246,9 +245,9 @@ public static SilentTokenCommandParameters createSilentTokenCommandParameters(
.forceRefresh(forceRefresh)
.account(parameters.getAccountRecord())
.authenticationScheme(authenticationScheme)
.scopes(new HashSet<>(parameters.getScopes()))
.powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled())
.correlationId(parameters.getCorrelationId())
.scopes(new HashSet<>(parameters.getScopes()))
.build();

return commandParameters;
Expand Down Expand Up @@ -294,50 +293,6 @@ public static DeviceCodeFlowCommandParameters createDeviceCodeFlowWithClaimsComm
return commandParameters;
}

/**
* Creates command parameter for [{@link com.microsoft.identity.common.nativeauth.internal.commands.AcquireTokenNoFixedScopesCommand}] of Native Auth.
*
* @param configuration PCA configuration
* @param tokenCache token cache for storing results
* @param accountRecord accountRecord object containing account information
* @param forceRefresh boolean parameter to denote if refresh should be forced
* @param correlationId correlation ID to use in the API request, taken from the previous API response in the flow
* @return Command parameter object
* @throws ClientException
*/
public static AcquireTokenNoFixedScopesCommandParameters createAcquireTokenNoFixedScopesCommandParameters(
@NonNull final PublicClientApplicationConfiguration configuration,
@NonNull final OAuth2TokenCache tokenCache,
@NonNull final AccountRecord accountRecord,
@NonNull final Boolean forceRefresh,
@NonNull final String correlationId) throws ClientException {
final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority());

final AbstractAuthenticationScheme authenticationScheme = new BearerAuthenticationSchemeInternal();

final AcquireTokenNoFixedScopesCommandParameters commandParameters = AcquireTokenNoFixedScopesCommandParameters
.builder()
.platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()))
.applicationName(configuration.getAppContext().getPackageName())
.applicationVersion(getPackageVersion(configuration.getAppContext()))
.clientId(configuration.getClientId())
.isSharedDevice(configuration.getIsSharedDevice())
.oAuth2TokenCache(tokenCache)
.redirectUri(configuration.getRedirectUri())
.requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion())
.sdkType(SdkType.MSAL)
.sdkVersion(PublicClientApplication.getSdkVersion())
.authority(authority)
.authenticationScheme(authenticationScheme)
.forceRefresh(forceRefresh)
.account(accountRecord)
.correlationId(correlationId)
.powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled())
.build();

return commandParameters;
}

public static DeviceCodeFlowCommandParameters createDeviceCodeFlowCommandParameters(
@NonNull final PublicClientApplicationConfiguration configuration,
@NonNull final OAuth2TokenCache tokenCache,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResu

internal class GetAccessTokenErrorTypes {
companion object {
const val NO_ACCOUNT_FOUND = "no_account_found"
/*
* The INVALID_SCOPES value indicates the scopes provided by the user are not valid
* If this occurs, valid scopes should be resubmitted
*/
const val INVALID_SCOPES = "invalid_scopes"

/*
* The NO_ACCOUNT_FOUND value indicates the user is not signed in.
* If this occurs, the API should be called after successful sign in
*/
SammyO marked this conversation as resolved.
Show resolved Hide resolved
const val NO_ACCOUNT_FOUND = "invalid_scopes"
}
}

Expand All @@ -40,4 +50,6 @@ class GetAccessTokenError(
override var exception: Exception? = null
): GetAccessTokenResult, Error(errorType = errorType, error = error, errorMessage= errorMessage, correlationId = correlationId, errorCodes = errorCodes, exception = exception) {
fun isNoAccountFound() : Boolean = this.errorType == GetAccessTokenErrorTypes.NO_ACCOUNT_FOUND

fun isInvalidScopes(): Boolean = this.errorType == GetAccessTokenErrorTypes.INVALID_SCOPES
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ import com.microsoft.identity.client.exception.MsalException
import com.microsoft.identity.client.internal.CommandParametersAdapter
import com.microsoft.identity.common.internal.commands.RemoveCurrentAccountCommand
import com.microsoft.identity.common.internal.controllers.LocalMSALController
import com.microsoft.identity.common.java.AuthenticationConstants
import com.microsoft.identity.common.java.commands.CommandCallback
import com.microsoft.identity.common.java.commands.SilentTokenCommand
import com.microsoft.identity.common.java.controllers.CommandDispatcher
import com.microsoft.identity.common.java.controllers.ExceptionAdapter
import com.microsoft.identity.common.java.dto.AccountRecord
Expand All @@ -46,7 +48,7 @@ import com.microsoft.identity.common.java.exception.ServiceException
import com.microsoft.identity.common.java.logging.LogSession
import com.microsoft.identity.common.java.logging.Logger
import com.microsoft.identity.common.java.result.ILocalAuthenticationResult
import com.microsoft.identity.common.nativeauth.internal.commands.AcquireTokenNoFixedScopesCommand
import com.microsoft.identity.common.java.result.LocalAuthenticationResult
import com.microsoft.identity.common.nativeauth.internal.controllers.NativeAuthMsalController
import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplication
import com.microsoft.identity.nativeauth.NativeAuthPublicClientApplicationConfiguration
Expand All @@ -60,6 +62,7 @@ import com.microsoft.identity.nativeauth.utils.serializable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in the technical design document review, the documentation in comments needs to be updated to explain the difference between these two methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments updated

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

/**
* Retrieves the access token for the currently signed in account from the cache.
* Retrieves the access token for the default OIDC(openid, offline_access, profile) scopes from the cache
SammyO marked this conversation as resolved.
Show resolved Hide resolved
* If the access token is expired, it will be attempted to be refreshed using the refresh token that's stored in the cache;
* callback variant.
*
* @return [com.microsoft.identity.client.IAuthenticationResult] If successful.
* @throws [MsalClientException] If the the account doesn't exist in the cache.
* @throws [ServiceException] If the refresh token doesn't exist in the cache/is expired, or the refreshing fails.
SammyO marked this conversation as resolved.
Show resolved Hide resolved
*/
fun getAccessToken(forceRefresh: Boolean = false, callback: GetAccessTokenCallback) {
LogSession.logMethodCall(
Expand All @@ -233,18 +234,70 @@ class AccountState private constructor(
}

/**
* Retrieves the access token for the currently signed in account from the cache.
* Retrieves the access token for the default OIDC(openid, offline_access, profile) scopes from the cache.
SammyO marked this conversation as resolved.
Show resolved Hide resolved
* If the access token is expired, it will be attempted to be refreshed using the refresh token that's stored in the cache;
* Kotlin coroutines variant.
*
* @return [com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult] The result of the getAccessToken action
*/
suspend fun getAccessToken(forceRefresh: Boolean = false): GetAccessTokenResult {
SammyO marked this conversation as resolved.
Show resolved Hide resolved
return getAccessTokenInternal(forceRefresh, AuthenticationConstants.DEFAULT_SCOPES.toList());
}

/**
* Retrieves the access token for the currently signed in account from the cache such that
* the scope of retrieved access token is a superset of requested scopes. If the access token
* has expired, it will be refreshed using the refresh token that's stored in the cache. If no
* access token matching the requested scopes is found in cache then a new access token is fetched.
* Kotlin coroutines variant.
*
* @return [com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult] The result of the getAccessToken action
*/
suspend fun getAccessToken(forceRefresh: Boolean = false, scopes: List<String>): GetAccessTokenResult {
if (scopes.isEmpty()) {
SammyO marked this conversation as resolved.
Show resolved Hide resolved
return GetAccessTokenError(
errorType = GetAccessTokenErrorTypes.INVALID_SCOPES,
errorMessage = "Empty or invalid scopes",
correlationId = correlationId
)
}

return getAccessTokenInternal(forceRefresh, scopes)
}

/**
* Retrieves the access token for the currently signed in account from the cache such that
* the scope of retrieved access token is a superset of requested scopes. If the access token
* has expired, it will be refreshed using the refresh token that's stored in the cache. If no
* access token matching the requested scopes is found in cache then a new access token is fetched.
* callback variant.
*
* @return [com.microsoft.identity.client.IAuthenticationResult] If successful.
*/
fun getAccessToken(forceRefresh: Boolean = false, scopes: List<String>, callback: GetAccessTokenCallback) {
SammyO marked this conversation as resolved.
Show resolved Hide resolved
SammyO marked this conversation as resolved.
Show resolved Hide resolved
LogSession.logMethodCall(
tag = TAG,
correlationId = null,
methodName = "$TAG.getAccessToken"
)
NativeAuthPublicClientApplication.pcaScope.launch {
try {
val result = getAccessToken(forceRefresh, scopes)
callback.onResult(result)
} catch (e: MsalException) {
Logger.error(TAG, "Exception thrown in getAccessToken", e)
callback.onError(e)
}
}
}

private suspend fun getAccessTokenInternal(forceRefresh: Boolean, scopes: List<String>): GetAccessTokenResult {
LogSession.logMethodCall(
tag = TAG,
correlationId = null,
methodName = "$TAG.getAccessToken(forceRefresh: Boolean)"
)

return withContext(Dispatchers.IO) {
try {
val currentAccount =
Expand All @@ -253,31 +306,45 @@ class AccountState private constructor(
errorType = GetAccessTokenErrorTypes.NO_ACCOUNT_FOUND,
error = MsalClientException.NO_CURRENT_ACCOUNT,
errorMessage = MsalClientException.NO_CURRENT_ACCOUNT_ERROR_MESSAGE,
correlationId = "UNSET"
correlationId = correlationId
)

val acquireTokenSilentParameters = AcquireTokenSilentParameters.Builder()
.forAccount(currentAccount)
.fromAuthority(currentAccount.authority)
.withCorrelationId(UUID.fromString(correlationId))
.forceRefresh(forceRefresh)
.withScopes(scopes)
.build()

val accountToBeUsed = PublicClientApplication.selectAccountRecordForTokenRequest(
val accountRecord = PublicClientApplication.selectAccountRecordForTokenRequest(
config,
acquireTokenSilentParameters
)
acquireTokenSilentParameters.accountRecord = accountRecord

val params =
CommandParametersAdapter.createAcquireTokenNoFixedScopesCommandParameters(
config,
config.oAuth2TokenCache,
accountToBeUsed,
forceRefresh,
correlationId
)
val params = CommandParametersAdapter.createSilentTokenCommandParameters(
config,
config.oAuth2TokenCache,
acquireTokenSilentParameters
)

val command = AcquireTokenNoFixedScopesCommand(
val command = SilentTokenCommand(
params,
NativeAuthMsalController(),
NativeAuthMsalController().asControllerFactory(),
object : CommandCallback<LocalAuthenticationResult?, BaseException?> {
override fun onError(error: BaseException?) {
// Do nothing, handled by CommandDispatcher.submitSilentReturningFuture()
}

override fun onTaskCompleted(result: LocalAuthenticationResult?) {
// Do nothing, handled by CommandDispatcher.submitSilentReturningFuture()
}

override fun onCancel() {
// Do nothing
}
},
PublicApiId.NATIVE_AUTH_ACCOUNT_GET_ACCESS_TOKEN
)

Expand All @@ -288,14 +355,14 @@ class AccountState private constructor(
is ServiceException -> {
GetAccessTokenError(
exception = ExceptionAdapter.convertToNativeAuthException(commandResult),
correlationId = "UNSET"
correlationId = correlationId
)
}

is Exception -> {
GetAccessTokenError(
exception = commandResult,
correlationId = "UNSET"
correlationId = correlationId
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should actually be using commandResult.correlationid here, for the unlikely case that the API has returned a different correlation ID. That field might be null, so we'd need to update GetAccessTokenError. And that improvement would have to be applied to more places.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change can be done when commandResult is ServiceException if commandResult is Exception then we will use the old correlationId.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to move this into a separate PR. It's not blocking.

)
}

Expand Down