Skip to content

Commit 1811fe5

Browse files
@W-16171409: [Android] Add QR Code Login Support in MSDK (#2594)
1 parent e9e5cad commit 1811fe5

3 files changed

Lines changed: 227 additions & 13 deletions

File tree

libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ open class SalesforceSDKManager protected constructor(
259259
field = value
260260
}
261261

262+
/** Indicates if login via QR Code and UI bridge API is enabled */
263+
@set:Synchronized
264+
open var isQrCodeLoginEnabled = false
265+
262266
/** Indicates if logout is in progress */
263267
var isLoggingOut = false
264268
private set
@@ -1558,6 +1562,7 @@ open class SalesforceSDKManager protected constructor(
15581562
open fun getInstance() = INSTANCE ?: throw RuntimeException("Apps must call SalesforceSDKManager.init() first.")
15591563

15601564
/** Allow Kotlin subclasses to set themselves as the instance. */
1565+
@Suppress("unused")
15611566
@JvmSynthetic
15621567
fun setInstance(subclass: SalesforceSDKManager) {
15631568
INSTANCE = subclass

libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt

Lines changed: 162 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ import com.salesforce.androidsdk.util.UriFragmentParser.parse
116116
import kotlinx.coroutines.CoroutineScope
117117
import kotlinx.coroutines.Dispatchers.IO
118118
import kotlinx.coroutines.launch
119+
import org.json.JSONObject
120+
import java.net.URLDecoder
119121
import com.salesforce.androidsdk.R.layout.sf__login as sf__login_layout
120122
import com.salesforce.androidsdk.R.menu.sf__login as sf__login_menu
121123

@@ -142,6 +144,9 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
142144

143145
val salesforceSDKManager = SalesforceSDKManager.getInstance()
144146

147+
// Determine if the activity was created from a deep link intent with QR code log in via UI bridge API parameters.
148+
val isDeepLinkedQrCodeLogin = isDeepLinkedQrCodeLogin(intent)
149+
145150
accountAuthenticatorResponse = intent.getParcelableExtra<AccountAuthenticatorResponse?>(
146151
KEY_ACCOUNT_AUTHENTICATOR_RESPONSE
147152
)?.apply {
@@ -157,14 +162,19 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
157162

158163
salesforceSDKManager.setViewNavigationVisibility(this)
159164

160-
// Get login options from the intent's extras
161-
val loginOptions = fromBundleWithSafeLoginUrl(intent.extras)
165+
// Determine login options for QR code login or the app's usual login.
166+
val loginOptions = when {
167+
isDeepLinkedQrCodeLogin -> salesforceSDKManager.loginOptions
168+
else -> fromBundleWithSafeLoginUrl(intent.extras)
169+
}
162170

163171
// Protect against screenshots
164172
window.setFlags(FLAG_SECURE, FLAG_SECURE)
165173

166-
// Fetch authentication configuration if required
167-
salesforceSDKManager.fetchAuthenticationConfiguration()
174+
// Fetch authentication configuration except for QR code login.
175+
if (!isDeepLinkedQrCodeLogin) {
176+
salesforceSDKManager.fetchAuthenticationConfiguration()
177+
}
168178

169179
// Setup content view
170180
setContentView(sf__login_layout)
@@ -207,7 +217,13 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
207217
LoginActivityCreateComplete,
208218
this
209219
)
210-
certAuthOrLogin()
220+
221+
// Prompt user with log in page or log in via other configurations such as QR code.
222+
when {
223+
isDeepLinkedQrCodeLogin -> loginFromQrCode("?" + intent.data?.query)
224+
else -> certAuthOrLogin()
225+
}
226+
211227
if (!receiverRegistered) {
212228
authConfigReceiver = AuthConfigReceiver().also { changeServerReceiver ->
213229
registerReceiver(
@@ -344,9 +360,9 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
344360
wasBackgrounded = false
345361
}
346362

347-
public override fun onSaveInstanceState(bundle: Bundle) {
348-
super.onSaveInstanceState(bundle)
349-
webviewHelper?.saveState(bundle)
363+
public override fun onSaveInstanceState(outState: Bundle) {
364+
super.onSaveInstanceState(outState)
365+
webviewHelper?.saveState(outState)
350366
}
351367

352368
override fun onKeyDown(keyCode: Int, event: KeyEvent) =
@@ -653,9 +669,147 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
653669

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

672+
// region QR Code Login Via UI Bridge API Public Implementation
673+
674+
/**
675+
* Automatically log in with a UI Bridge API login QR code.
676+
* @param loginQrCodeContent The login QR code content. This should be
677+
* either a URL or URL query containing the UI Bridge API JSON parameter.
678+
* The UI Bridge API JSON parameter should contain URL-encoded JSON with two
679+
* values:
680+
* - frontdoor_bridge_url
681+
* - pkce_code_verifier
682+
*
683+
* If pkce_code_verifier is not specified then the user agent flow is used
684+
* @return Boolean true if a log in attempt is possible using the provided QR
685+
* code content, false otherwise
686+
*/
687+
fun loginFromQrCode(
688+
loginQrCodeContent: String?
689+
) = uiBridgeApiParametersFromLoginQrCodeContent(
690+
loginQrCodeContent
691+
)?.let { uiBridgeApiParameters ->
692+
loginWithFrontdoorBridgeUrl(
693+
uiBridgeApiParameters.frontdoorBridgeUrl,
694+
uiBridgeApiParameters.pkceCodeVerifier
695+
)
696+
true
697+
} ?: false
698+
699+
/**
700+
* Automatically log in with a UI Bridge API front door bridge URL and PKCE
701+
* code verifier.
702+
* @param frontdoorBridgeUrl The UI Bridge API front door bridge URL
703+
* @param pkceCodeVerifier The PKCE code verifier
704+
*/
705+
@Suppress("MemberVisibilityCanBePrivate")
706+
fun loginWithFrontdoorBridgeUrl(
707+
frontdoorBridgeUrl: String,
708+
pkceCodeVerifier: String?
709+
) = webviewHelper?.loginWithFrontdoorBridgeUrl(frontdoorBridgeUrl, pkceCodeVerifier)
710+
711+
// endregion
712+
// region QR Code Login Via UI Bridge API Private Implementation
713+
714+
/**
715+
* Determines if QR code login is enabled for the provided intent.
716+
* @param intent The intent to determine QR code login enablement for
717+
* @return Boolean true if QR code login is enabled for the the intent or
718+
* false otherwise
719+
*/
720+
private fun isDeepLinkedQrCodeLogin(
721+
intent: Intent
722+
) = SalesforceSDKManager.getInstance().isQrCodeLoginEnabled
723+
&& intent.data?.path?.contains(LOGIN_QR_PATH) == true
724+
725+
/**
726+
* Parses UI Bridge API parameters from the provided login QR code content.
727+
* @param loginQrCodeContent The login QR code content string
728+
* @return UiBridgeApiParameters: The UI Bridge API parameters or null if the QR code
729+
* content cannot provide them for any reason
730+
*/
731+
private fun uiBridgeApiParametersFromLoginQrCodeContent(
732+
loginQrCodeContent: String?
733+
) = loginQrCodeContent?.let { loginQrCodeContentUnwrapped ->
734+
uiBridgeApiJsonFromQrCodeContent(loginQrCodeContentUnwrapped)?.let { uiBridgeApiJson ->
735+
uiBridgeApiParametersFromUiBridgeApiJson(uiBridgeApiJson)
736+
}
737+
}
738+
739+
/**
740+
* Parses UI Bridge API parameters JSON from the provided string, which may
741+
* be formatted to match either QR code content provided by the intent or
742+
* the app's QR code library.
743+
*
744+
* 1. From intent (external QR reader): ?bridgeJson={...}
745+
* 2. From the app's QR reader: ?bridgeJson=%7B...%7D
746+
*
747+
* @param qrCodeContent The QR code content string
748+
* @return String: The UI Bridge API parameter JSON or null if the string
749+
* cannot provide the JSON for any reason
750+
*/
751+
private fun uiBridgeApiJsonFromQrCodeContent(
752+
qrCodeContent: String
753+
) = qrCodeBridgeJsonRegexExternal.find(qrCodeContent)?.groups?.get(1)?.value
754+
?: qrCodeBridgeJsonRegexInternal.find(qrCodeContent)?.groups?.get(1)?.value?.let {
755+
URLDecoder.decode(it, "UTF-8")
756+
}
757+
758+
/**
759+
* Creates UI Bridge API parameters from the provided JSON string.
760+
* @param uiBridgeApiParameterJsonString The UI Bridge API parameters JSON
761+
* string
762+
* @return The UI Bridge API parameters
763+
*/
764+
private fun uiBridgeApiParametersFromUiBridgeApiJson(
765+
uiBridgeApiParameterJsonString: String
766+
) = JSONObject(uiBridgeApiParameterJsonString).let { uiBridgeApiParameterJson ->
767+
UiBridgeApiParameters(
768+
uiBridgeApiParameterJson.getString(FRONTDOOR_BRIDGE_URL_KEY),
769+
when (uiBridgeApiParameterJson.has(PKCE_CODE_VERIFIER_KEY)) {
770+
true -> uiBridgeApiParameterJson.optString(PKCE_CODE_VERIFIER_KEY)
771+
else -> null
772+
}
773+
)
774+
}
775+
776+
/**
777+
* A data class representing UI Bridge API parameters provided by a login QR
778+
* code.
779+
*/
780+
private data class UiBridgeApiParameters(
781+
782+
/** The front door bridge URL provided by the login QR code */
783+
val frontdoorBridgeUrl: String,
784+
785+
/** The PKCE code verifier provided by the login QR code */
786+
val pkceCodeVerifier: String?
787+
)
788+
789+
// endregion
790+
656791
companion object {
657792
const val PICK_SERVER_REQUEST_CODE = 10
658793
private const val SETUP_REQUEST_CODE = 72
659794
private const val TAG = "LoginActivity"
795+
796+
// region QR Code Login Via UI Bridge API Constants
797+
798+
/** The QR code login intent path */
799+
private const val LOGIN_QR_PATH = "/login/qr"
800+
801+
/** The login QR code's UI Bridge API parameter's JSON frontdoor bridge URL key */
802+
private const val FRONTDOOR_BRIDGE_URL_KEY = "frontdoor_bridge_url"
803+
804+
/** The login QR code's UI Bridge API parameter's JSON PKCE code verifier key */
805+
private const val PKCE_CODE_VERIFIER_KEY = "pkce_code_verifier"
806+
807+
/** A regular expression to extract the UI Bridge API parameter JSON from the intent's login QR code content */
808+
private val qrCodeBridgeJsonRegexExternal by lazy { """\?bridgeJson=(\{.*\})""".toRegex() }
809+
810+
/** A regular expression to extract the UI Bridge API parameter JSON from the app's login QR code content */
811+
private val qrCodeBridgeJsonRegexInternal by lazy { """\?bridgeJson=(%7B.*%7D)""".toRegex() }
812+
813+
// endregion
660814
}
661815
}

libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.kt

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,23 @@ import java.util.function.Consumer
146146
* @Deprecated This class will no longer be public starting in Mobile SDK 13.0. It
147147
* is no longer necessary to extend or change LoginActivity's instance of this class
148148
* to support multi-factor authentication. If there are other uses cases please
149-
* inform the team via Github or our Trailblazer community.
149+
* inform the team via Github or our Trailblazer community.
150150
*/
151151
@Deprecated(
152152
"This class will no longer be public starting in Mobile SDK 13.0.",
153153
level = DeprecationLevel.WARNING,
154154
)
155155
open class OAuthWebviewHelper : KeyChainAliasCallback {
156156

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

160+
/** For Salesforce Identity UI Bridge API support, an overriding front door bridge URL to use in place of the default initial URL */
161+
private var isUsingFrontDoorBridge = false
162+
163+
/** 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` */
164+
private var frontDoorBridgeCodeVerifier: String? = null
165+
159166
/**
160167
* The host activity/fragment should pass in an implementation of this
161168
* interface so that it can notify it of things it needs to do as part of
@@ -322,6 +329,10 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
322329
) {
323330
val instance = SalesforceSDKManager.getInstance()
324331

332+
// Reset state from previous log in attempt.
333+
// - Salesforce Identity UI Bridge API log in, such as QR code log in.
334+
resetFrontDoorBridgeUrl()
335+
325336
e(TAG, "$error: $errorDesc", e)
326337

327338
// Broadcast a notification that the authentication flow failed
@@ -514,6 +525,11 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
514525
useWebServerAuthentication: Boolean,
515526
useHybridAuthentication: Boolean
516527
): URI {
528+
529+
// Reset log in state,
530+
// - Salesforce Identity UI Bridge API log in, such as QR code log in.
531+
resetFrontDoorBridgeUrl()
532+
517533
val loginOptions = loginOptions
518534
val oAuthClientId = oAuthClientId
519535
val authorizationDisplayType = authorizationDisplayType
@@ -671,9 +687,16 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
671687
null
672688
)
673689

674-
else -> when {
675-
instance.useWebServerAuthentication -> onWebServerFlowComplete(params["code"])
676-
else -> onAuthFlowComplete(TokenEndpointResponse(params))
690+
else -> {
691+
// Determine if presence of override parameters require the user agent flow.
692+
val overrideWithUserAgentFlow = isUsingFrontDoorBridge && frontDoorBridgeCodeVerifier == null
693+
when {
694+
instance.useWebServerAuthentication && !overrideWithUserAgentFlow ->
695+
onWebServerFlowComplete(params["code"])
696+
697+
else ->
698+
onAuthFlowComplete(TokenEndpointResponse(params))
699+
}
677700
}
678701
}
679702
}
@@ -729,6 +752,11 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
729752
*/
730753
open fun onAuthFlowComplete(tr: TokenEndpointResponse?, nativeLogin: Boolean = false) {
731754
CoroutineScope(IO).launch {
755+
756+
// Reset log in state,
757+
// - Salesforce Identity UI Bridge API log in, such as QR code log in.
758+
resetFrontDoorBridgeUrl()
759+
732760
FinishAuthTask().execute(tr, nativeLogin)
733761
}
734762
}
@@ -748,7 +776,7 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
748776
create(loginOptions.loginUrl),
749777
loginOptions.oauthClientId,
750778
code,
751-
codeVerifier,
779+
frontDoorBridgeCodeVerifier ?: codeVerifier,
752780
loginOptions.oauthCallbackUrl
753781
)
754782
}.onFailure { throwable ->
@@ -1181,6 +1209,33 @@ open class OAuthWebviewHelper : KeyChainAliasCallback {
11811209
}
11821210
}
11831211

1212+
/**
1213+
* Automatically log in using the provided UI Bridge API parameters.
1214+
* @param frontdoorBridgeUrl The UI Bridge API front door bridge API
1215+
* @param pkceCodeVerifier The PKCE code verifier
1216+
*/
1217+
fun loginWithFrontdoorBridgeUrl(
1218+
frontdoorBridgeUrl: String,
1219+
pkceCodeVerifier: String?
1220+
) {
1221+
isUsingFrontDoorBridge = true
1222+
1223+
val uri = URI(frontdoorBridgeUrl)
1224+
loginOptions.loginUrl = "${uri.scheme}://${uri.host}"
1225+
frontDoorBridgeCodeVerifier = pkceCodeVerifier
1226+
1227+
webView?.loadUrl(frontdoorBridgeUrl)
1228+
}
1229+
1230+
/**
1231+
* Resets all state related to Salesforce Identity API UI Bridge front door bridge URL log in to
1232+
* its default inactive state.
1233+
*/
1234+
private fun resetFrontDoorBridgeUrl() {
1235+
isUsingFrontDoorBridge = false
1236+
frontDoorBridgeCodeVerifier = null
1237+
}
1238+
11841239
companion object {
11851240

11861241
/**

0 commit comments

Comments
 (0)