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

@W-16171409: [Android] Add QR Code Login Support in MSDK #2594

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ba16f74
(WIP) login with QR button in login screen
wmathurin May 31, 2024
fb4baef
Parsing bridge url and pkce code verifier from url read from QR
wmathurin Jun 6, 2024
9a0a352
Enabling QR login in Rest Explorer
wmathurin Jun 6, 2024
4396670
Loading frontdoor bridge url in web view (wip)
wmathurin Jun 6, 2024
ca4d874
Adding custom scheme handler for login qr
wmathurin Jun 6, 2024
edf8c6a
Handling custom scheme (wip)
wmathurin Jun 7, 2024
3c9c058
Moving QR support code our of Mobile SDK (to template)
wmathurin Jun 7, 2024
4bb050c
During QR login, do no load anything in the web view and don't get au…
wmathurin Jun 7, 2024
43e03d9
Handling url encoded bridgeJson
wmathurin Jun 7, 2024
e2bbfec
Extracting login url from front door bridge url
wmathurin Jun 8, 2024
7d28d9d
@W-16171409: [Android] Add QR Code Login Support in MSDK (Light Cleanup)
JohnsonEricAtSalesforce Jul 18, 2024
57dc2c1
@W-16171409: [Android] Add QR Code Login Support in MSDK (Light Code …
JohnsonEricAtSalesforce Jul 22, 2024
f9ea0e5
@W-16171409: [Android] Add QR Code Login Support in MSDK (Comprehensi…
JohnsonEricAtSalesforce Jul 22, 2024
6f0974e
@W-16171409: [Android] Add QR Code Login Support in MSDK (Logic allow…
JohnsonEricAtSalesforce Sep 29, 2024
42f440f
@W-16171409: [Android] Add QR Code Login Support in MSDK (Reduce Comp…
JohnsonEricAtSalesforce Oct 1, 2024
74d1572
@W-16171409: [Android] Add QR Code Login Support in MSDK (Reduce Comp…
JohnsonEricAtSalesforce Oct 1, 2024
a2a3575
@W-16171409: [Android] Add QR Code Login Support in MSDK (Reset Front…
JohnsonEricAtSalesforce Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ open class SalesforceSDKManager protected constructor(
field = value
}

/** Indicates if login via QR Code and UI bridge API is enabled */
@set:Synchronized
open var isQrCodeLoginEnabled = false

/** Indicates if logout is in progress */
var isLoggingOut = false
private set
Expand Down Expand Up @@ -1557,6 +1561,7 @@ open class SalesforceSDKManager protected constructor(
open fun getInstance() = INSTANCE ?: throw RuntimeException("Apps must call SalesforceSDKManager.init() first.")

/** Allow Kotlin subclasses to set themselves as the instance. */
@Suppress("unused")
@JvmSynthetic
fun setInstance(subclass: SalesforceSDKManager) {
INSTANCE = subclass
Expand Down
170 changes: 162 additions & 8 deletions libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ import com.salesforce.androidsdk.util.UriFragmentParser.parse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.net.URLDecoder
import com.salesforce.androidsdk.R.layout.sf__login as sf__login_layout
import com.salesforce.androidsdk.R.menu.sf__login as sf__login_menu

Expand Down Expand Up @@ -149,6 +151,9 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {

val salesforceSDKManager = SalesforceSDKManager.getInstance()

// Determine if the activity was created from a deep link intent with QR code log in via UI bridge API parameters.
val isDeepLinkedQrCodeLogin = isDeepLinkedQrCodeLogin(intent)

accountAuthenticatorResponse = intent.getParcelableExtra<AccountAuthenticatorResponse?>(
KEY_ACCOUNT_AUTHENTICATOR_RESPONSE
)?.apply {
Expand All @@ -164,14 +169,19 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {

salesforceSDKManager.setViewNavigationVisibility(this)

// Get login options from the intent's extras
val loginOptions = fromBundleWithSafeLoginUrl(intent.extras)
// Determine login options for QR code login or the app's usual login.
val loginOptions = when {
isDeepLinkedQrCodeLogin -> salesforceSDKManager.loginOptions
else -> fromBundleWithSafeLoginUrl(intent.extras)
}

// Protect against screenshots
window.setFlags(FLAG_SECURE, FLAG_SECURE)

// Fetch authentication configuration if required
salesforceSDKManager.fetchAuthenticationConfiguration()
// Fetch authentication configuration except for QR code login.
if (!isDeepLinkedQrCodeLogin) {
salesforceSDKManager.fetchAuthenticationConfiguration()
}
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved

// Setup content view
setContentView(sf__login_layout)
Expand Down Expand Up @@ -214,7 +224,13 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
LoginActivityCreateComplete,
this
)
certAuthOrLogin()

// Prompt user with log in page or log in via other configurations such as QR code.
when {
isDeepLinkedQrCodeLogin -> loginFromQrCode("?" + intent.data?.query)
else -> certAuthOrLogin()
}

if (!receiverRegistered) {
authConfigReceiver = AuthConfigReceiver().also { changeServerReceiver ->
registerReceiver(
Expand Down Expand Up @@ -345,9 +361,9 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
wasBackgrounded = false
}

public override fun onSaveInstanceState(bundle: Bundle) {
super.onSaveInstanceState(bundle)
webviewHelper?.saveState(bundle)
public override fun onSaveInstanceState(outState: Bundle) {
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved
super.onSaveInstanceState(outState)
webviewHelper?.saveState(outState)
}

override fun onKeyDown(keyCode: Int, event: KeyEvent) =
Expand Down Expand Up @@ -654,9 +670,147 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {

open fun onBioAuthClick(view: View?) = presentBiometric()

// region QR Code Login Via UI Bridge API Public Implementation

/**
* Automatically log in with a UI Bridge API login QR code.
* @param loginQrCodeContent The login QR code content. This should be
* either a URL or URL query containing the UI Bridge API JSON parameter.
* The UI Bridge API JSON parameter should contain URL-encoded JSON with two
* values:
* - frontdoor_bridge_url
* - pkce_code_verifier
*
* If pkce_code_verifier is not specified then the user agent flow is used
* @return Boolean true if a log in attempt is possible using the provided QR
* code content, false otherwise
*/
fun loginFromQrCode(
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved
loginQrCodeContent: String?
) = uiBridgeApiParametersFromLoginQrCodeContent(
loginQrCodeContent
)?.let { uiBridgeApiParameters ->
loginWithFrontdoorBridgeUrl(
uiBridgeApiParameters.frontdoorBridgeUrl,
uiBridgeApiParameters.pkceCodeVerifier
)
true
} ?: false

/**
* Automatically log in with a UI Bridge API front door bridge URL and PKCE
* code verifier.
* @param frontdoorBridgeUrl The UI Bridge API front door bridge URL
* @param pkceCodeVerifier The PKCE code verifier
*/
@Suppress("MemberVisibilityCanBePrivate")
fun loginWithFrontdoorBridgeUrl(
frontdoorBridgeUrl: String,
pkceCodeVerifier: String?
) = webviewHelper?.loginWithFrontdoorBridgeUrl(frontdoorBridgeUrl, pkceCodeVerifier)

// endregion
// region QR Code Login Via UI Bridge API Private Implementation

/**
* Determines if QR code login is enabled for the provided intent.
* @param intent The intent to determine QR code login enablement for
* @return Boolean true if QR code login is enabled for the the intent or
* false otherwise
*/
private fun isDeepLinkedQrCodeLogin(
intent: Intent
) = SalesforceSDKManager.getInstance().isQrCodeLoginEnabled
&& intent.data?.path?.contains(LOGIN_QR_PATH) == true

/**
* Parses UI Bridge API parameters from the provided login QR code content.
* @param loginQrCodeContent The login QR code content string
* @return UiBridgeApiParameters: The UI Bridge API parameters or null if the QR code
* content cannot provide them for any reason
*/
private fun uiBridgeApiParametersFromLoginQrCodeContent(
loginQrCodeContent: String?
) = loginQrCodeContent?.let { loginQrCodeContentUnwrapped ->
uiBridgeApiJsonFromQrCodeContent(loginQrCodeContentUnwrapped)?.let { uiBridgeApiJson ->
uiBridgeApiParametersFromUiBridgeApiJson(uiBridgeApiJson)
}
}

/**
* Parses UI Bridge API parameters JSON from the provided string, which may
* be formatted to match either QR code content provided by the intent or
* the app's QR code library.
*
* 1. From intent (external QR reader): ?bridgeJson={...}
* 2. From the app's QR reader: ?bridgeJson=%7B...%7D
*
* @param qrCodeContent The QR code content string
* @return String: The UI Bridge API parameter JSON or null if the string
* cannot provide the JSON for any reason
*/
private fun uiBridgeApiJsonFromQrCodeContent(
qrCodeContent: String
) = qrCodeBridgeJsonRegexExternal.find(qrCodeContent)?.groups?.get(1)?.value
?: qrCodeBridgeJsonRegexInternal.find(qrCodeContent)?.groups?.get(1)?.value?.let {
URLDecoder.decode(it, "UTF-8")
}

/**
* Creates UI Bridge API parameters from the provided JSON string.
* @param uiBridgeApiParameterJsonString The UI Bridge API parameters JSON
* string
* @return The UI Bridge API parameters
*/
private fun uiBridgeApiParametersFromUiBridgeApiJson(
uiBridgeApiParameterJsonString: String
) = JSONObject(uiBridgeApiParameterJsonString).let { uiBridgeApiParameterJson ->
UiBridgeApiParameters(
uiBridgeApiParameterJson.getString(FRONTDOOR_BRIDGE_URL_KEY),
when (uiBridgeApiParameterJson.has(PKCE_CODE_VERIFIER_KEY)) {
true -> uiBridgeApiParameterJson.optString(PKCE_CODE_VERIFIER_KEY)
else -> null
}
)
}

/**
* A data class representing UI Bridge API parameters provided by a login QR
* code.
*/
private data class UiBridgeApiParameters(

/** The front door bridge URL provided by the login QR code */
val frontdoorBridgeUrl: String,

/** The PKCE code verifier provided by the login QR code */
val pkceCodeVerifier: String?
)

// endregion

companion object {
const val PICK_SERVER_REQUEST_CODE = 10
private const val SETUP_REQUEST_CODE = 72
private const val TAG = "LoginActivity"

// region QR Code Login Via UI Bridge API Constants

/** The QR code login intent path */
private const val LOGIN_QR_PATH = "/login/qr"

/** The login QR code's UI Bridge API parameter's JSON frontdoor bridge URL key */
private const val FRONTDOOR_BRIDGE_URL_KEY = "frontdoor_bridge_url"

/** The login QR code's UI Bridge API parameter's JSON PKCE code verifier key */
private const val PKCE_CODE_VERIFIER_KEY = "pkce_code_verifier"

/** A regular expression to extract the UI Bridge API parameter JSON from the intent's login QR code content */
private val qrCodeBridgeJsonRegexExternal by lazy { """\?bridgeJson=(\{.*\})""".toRegex() }

/** A regular expression to extract the UI Bridge API parameter JSON from the app's login QR code content */
private val qrCodeBridgeJsonRegexInternal by lazy { """\?bridgeJson=(%7B.*%7D)""".toRegex() }

// endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,23 @@ import java.util.function.Consumer
* @Deprecated This class will no longer be public starting in Mobile SDK 13.0. It
* is no longer necessary to extend or change LoginActivity's instance of this class
* to support multi-factor authentication. If there are other uses cases please
* inform the team via Github or our Trailblazer community.
* inform the team via Github or our Trailblazer community.
*/
@Deprecated(
"This class will no longer be public starting in Mobile SDK 13.0.",
level = DeprecationLevel.WARNING,
)
open class OAuthWebviewHelper : KeyChainAliasCallback {

/** The default, locally generated code verifier */
private var codeVerifier: String? = null

/** For Salesforce Identity UI Bridge API support, an overriding front door bridge URL to use in place of the default initial URL */
private var isUsingFrontDoorBridge = false

/** For Salesforce Identity UI Bridge API support, the optional web server flow code verifier accompanying the front door bridge URL. This can only be used with `overrideWithFrontDoorBridgeUrl` */
private var frontDoorBridgeCodeVerifier: String? = null

/**
* The host activity/fragment should pass in an implementation of this
* interface so that it can notify it of things it needs to do as part of
Expand Down Expand Up @@ -326,6 +333,10 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
) {
val instance = SalesforceSDKManager.getInstance()

// Reset state from previous log in attempt.
// - Salesforce Identity UI Bridge API log in, such as QR code log in.
resetFrontDoorBridgeUrl()

e(TAG, "$error: $errorDesc", e)

// Broadcast a notification that the authentication flow failed
Expand Down Expand Up @@ -503,6 +514,11 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
useWebServerAuthentication: Boolean,
useHybridAuthentication: Boolean
): URI {

// Reset log in state,
// - Salesforce Identity UI Bridge API log in, such as QR code log in.
resetFrontDoorBridgeUrl()

val loginOptions = loginOptions
val oAuthClientId = oAuthClientId
val authorizationDisplayType = authorizationDisplayType
Expand Down Expand Up @@ -660,9 +676,16 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
null
)

else -> when {
instance.useWebServerAuthentication -> onWebServerFlowComplete(params["code"])
else -> onAuthFlowComplete(TokenEndpointResponse(params))
else -> {
// Determine if presence of override parameters require the user agent flow.
val overrideWithUserAgentFlow = isUsingFrontDoorBridge && frontDoorBridgeCodeVerifier == null
when {
instance.useWebServerAuthentication && !overrideWithUserAgentFlow ->
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved
onWebServerFlowComplete(params["code"])

else ->
onAuthFlowComplete(TokenEndpointResponse(params))
}
}
}
}
Expand Down Expand Up @@ -719,6 +742,11 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
open fun onAuthFlowComplete(tr: TokenEndpointResponse?, nativeLogin: Boolean = false) {
d(TAG, "token response -> $tr")
CoroutineScope(IO).launch {

// Reset log in state,
// - Salesforce Identity UI Bridge API log in, such as QR code log in.
resetFrontDoorBridgeUrl()

FinishAuthTask().execute(tr, nativeLogin)
}
}
Expand All @@ -738,7 +766,7 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
create(loginOptions.loginUrl),
loginOptions.oauthClientId,
code,
codeVerifier,
frontDoorBridgeCodeVerifier ?: codeVerifier,
JohnsonEricAtSalesforce marked this conversation as resolved.
Show resolved Hide resolved
loginOptions.oauthCallbackUrl
)
}.onFailure { throwable ->
Expand Down Expand Up @@ -1399,6 +1427,33 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
}
}

/**
* Automatically log in using the provided UI Bridge API parameters.
* @param frontdoorBridgeUrl The UI Bridge API front door bridge API
* @param pkceCodeVerifier The PKCE code verifier
*/
fun loginWithFrontdoorBridgeUrl(
frontdoorBridgeUrl: String,
pkceCodeVerifier: String?
) {
isUsingFrontDoorBridge = true

val uri = URI(frontdoorBridgeUrl)
loginOptions.loginUrl = "${uri.scheme}://${uri.host}"
frontDoorBridgeCodeVerifier = pkceCodeVerifier

webView?.loadUrl(frontdoorBridgeUrl)
}

/**
* Resets all state related to Salesforce Identity API UI Bridge front door bridge URL log in to
* its default inactive state.
*/
private fun resetFrontDoorBridgeUrl() {
isUsingFrontDoorBridge = false
frontDoorBridgeCodeVerifier = null
}

companion object {

/**
Expand Down