What does “mobile-to-web cross login with a QR code” mean? It is one of the approaches for two-factor authentication. Suppose that a user is already authenticated in your application (in our example, it would be an android app) and the user wants to use their actual session to perform an automatic authentication in another application (in my case it’s a web app).
There are several examples of popular apps which use this approach. For example, to login into a web session with WhatsАpp, you must log in on your phone and then scan a QR code in the web interface.
The content of this article assumes:
The overall process consists of the following steps:
The final project, which demonstrates this approach (both the Android and web apps), can be downloaded from here: LoginWithQR.zip. The archive includes a ready-to-use Android project and a simple web project (as a single html file). To get the demo running, you should do the following:
Let’s get down to the most interesting things.
We’ve based the implementation of this example on the code that was demonstrated in the How to generate a QR code with Backendless API Service article. We have changed the method generateQRCodePicture and created generateQRCodeForLogin. These methods will help us with creating QR codes. The method getUserId will be used to exchange userToken for the user’s objectId. You can see the changes below.
Do not forget to change the package name (first line of the code). Then use CodeRunner to deploy the code to Backendless.
package <<YOUR-PACKAGE-NAME>>; import com.backendless.Backendless; import com.backendless.servercode.BackendlessService; import com.google.zxing.BarcodeFormat; import com.google.zxing.WriterException; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.UUID; @BackendlessService public class QRCodeGenerator { // in this method the work mainly is with the 'ZXing' library public byte[] generateQRCode( String data, int width, int height ) throws IOException, WriterException { QRCodeWriter qrCodeWriter = new QRCodeWriter(); BitMatrix bitMatrix = qrCodeWriter.encode( data, BarcodeFormat.QR_CODE, width, height ); byte[] png; try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { MatrixToImageWriter.writeToStream( bitMatrix, "PNG", baos ); png = baos.toByteArray(); } return png; } public HashMap<String, String> generateQRCodePicture( String data, int width, int height ) throws WriterException, IOException { // generate qr code with our method byte[] png = this.generateQRCode( data, width, height ); // generate random uuid as file name and save it using Backendless File service String uuid = UUID.randomUUID().toString(); String path = Backendless.Files.saveFile( "qr_codes", uuid + ".png", png ); // return response with the raw data (that was encoded) and file path (to qr code) HashMap<String, String> record = new HashMap<>(); record.put( "data", data ); record.put( "file", path ); return record; } public HashMap<String, String> generateQRCodeForLogin() throws IOException, WriterException { return generateQRCodePicture( UUID.randomUUID().toString(), 250, 250 ); } public String getUserId() { return Backendless.UserService.loggedInUser(); } }
Once the code is deployed to Backendless, make sure it works as expected:
Install the application for QR code scanning. We used the Barcode Scanner application. Here is the API for it.
If you are not using the complete project code we shared at the beginning of this article, you would need to setup an Android project using Android Studio. Alternatively, you could use the project archive mentioned above.
The project must reference two additional libraries. The first one is ‘backendless’ and the second one is ‘socket.io-client’, which is required to enable the real-time capability available in Backendless version 5.0. Add the following dependencies to the build.gradle file:
// https://mvnrepository.com/artifact/com.backendless/backendless implementation group: 'com.backendless', name: 'backendless', version: '5.0.+' // https://mvnrepository.com/artifact/io.socket/socket.io-client implementation group: 'io.socket', name: 'socket.io-client', version: '1.0.0'
Do not forget to add the INTERNET permission in the Android manifest file.
<uses-permission android:name="android.permission.INTERNET" />
This is the listing of the MainActivity class in the Android app. Pay attention to the highlighted code:
package com.examples.backendless; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import com.backendless.Backendless; import com.backendless.BackendlessUser; import com.backendless.HeadersManager; import com.backendless.async.callback.AsyncCallback; import com.backendless.exceptions.BackendlessFault; import com.backendless.messaging.MessageStatus; import java.util.Map; public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private static final String PROPERTY_STORAGE = MainActivity.class.getName(); private static final String USER_NAME_KEY = "userName"; private static final String USER_PASSWORD_KEY = "userPassword"; private static final String USER_TOKEN_KEY = "userToken"; private static final int rc_ScanQR = 1; public static final Uri zxingUri = Uri.parse("https://play.google.com/store/apps/details?id=com.google.zxing.client.android"); private static final String ZXING_PACKAGE = "com.google.zxing.client.android"; private static final String appId = YOUR-APP-ID; private static final String apiKey = YOUR-ANDROID-API-KEY; private EditText editText_name; private EditText editText_password; private EditText editText_userInfo; private Button button_login_logout; private Button button_loginWithQR; private String channelName = null; private String userName = null; private String userPassword = null; private String userToken = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initUI(); Backendless.initApp(this.getApplicationContext(), appId, apiKey); if (!isPackageInstalled(ZXING_PACKAGE)) { editText_name.setEnabled(false); editText_password.setEnabled(false); editText_userInfo.setEnabled(false); button_login_logout.setEnabled(false); button_loginWithQR.setEnabled(false); Handler handler = new Handler(); handler.postDelayed(() -> { MainActivity.this.runOnUiThread(() -> { if (getCurrentFocus() != null) { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); } View view = findViewById(R.id.main_activity); Snackbar snackbar = Snackbar.make(view, "Unable to find QR scanner app. Please make sure to install the 'Barcode Scanner' app by ZXing Team", Snackbar.LENGTH_INDEFINITE); View snackbarView = snackbar.getView(); TextView textView = (TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text); textView.setMaxLines(5); snackbar.setAction("Install", this::installZXing); snackbar.show(); }); }, 1500); } } private void initUI() { editText_name = findViewById(R.id.editText_name); editText_password = findViewById(R.id.editText_password); editText_userInfo = findViewById(R.id.editText_userInfo); button_login_logout = findViewById(R.id.button_login); button_loginWithQR = findViewById(R.id.button_loginWithQR); button_loginWithQR.setOnClickListener(this::scanDataFromQRCode); } @Override protected void onPause() { super.onPause(); SharedPreferences sharedPreferences = getSharedPreferences(PROPERTY_STORAGE, MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(USER_NAME_KEY, this.userName); editor.putString(USER_PASSWORD_KEY, this.userPassword); editor.putString(USER_TOKEN_KEY, this.userToken); editor.apply(); Log.i(MainActivity.class.getSimpleName(), "onPause: saved data successfully [userName=" + this.userName + ", userPassword=" + this.userPassword + ", userToken=" + this.userToken + "]"); } @Override protected void onPostResume() { super.onPostResume(); SharedPreferences sharedPreferences = getSharedPreferences(PROPERTY_STORAGE, MODE_PRIVATE); this.userName = sharedPreferences.getString(USER_NAME_KEY, null); this.editText_name.setText(this.userName); this.userPassword = sharedPreferences.getString(USER_PASSWORD_KEY, null); this.editText_password.setText(this.userPassword); this.userToken = sharedPreferences.getString(USER_TOKEN_KEY, null); Log.i(MainActivity.class.getSimpleName(), "onPostResume: restore data successfully [userName=" + this.userName + ", userPassword=" + this.userPassword + ", userToken=" + this.userToken + "]"); if (userToken != null) { button_loginWithQR.setVisibility(View.VISIBLE); button_login_logout.setText("Logout"); button_login_logout.setOnClickListener(this::backendlessLogout); } else { button_login_logout.setOnClickListener(this::backendlessLogin); } if (channelName != null) loginWithQRCode(this.channelName); } @Override protected void onStart() { super.onStart(); Log.i(MainActivity.class.getSimpleName(), "onStart: "); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); Log.i(MainActivity.class.getSimpleName(), "onRestoreInstanceState: "); } private void backendlessLogin(View view) { this.userName = editText_name.getText().toString(); this.userPassword = editText_password.getText().toString(); Backendless.UserService.login(MainActivity.this.userName, MainActivity.this.userPassword, new AsyncCallback<BackendlessUser>() { @Override public void handleResponse(BackendlessUser response) { button_loginWithQR.setVisibility(View.VISIBLE); button_login_logout.setText("Logout"); button_login_logout.setOnClickListener(MainActivity.this::backendlessLogout); StringBuilder sb = new StringBuilder(); for (Map.Entry<String, Object> property : response.getProperties().entrySet()) sb.append(property.getKey()).append(" : ").append(property.getValue()).append('\n'); editText_userInfo.setText(sb.toString()); userToken = HeadersManager.getInstance().getHeader(HeadersManager.HeadersEnum.USER_TOKEN_KEY); Log.i(MainActivity.class.getSimpleName(), "backendlessLogin [userToken=" + MainActivity.this.userToken + "]"); } @Override public void handleFault(BackendlessFault fault) { userToken = null; editText_userInfo.setText(fault.getCode() + '\n' + fault.getMessage() + '\n' + fault.getDetail()); } }); } private void backendlessLogout(View view) { Backendless.UserService.logout(new AsyncCallback<Void>() { @Override public void handleResponse(Void response) { userToken = null; button_loginWithQR.setVisibility(View.INVISIBLE); button_login_logout.setText("Login to Backendless"); button_login_logout.setOnClickListener(MainActivity.this::backendlessLogin); editText_userInfo.setText(""); } @Override public void handleFault(BackendlessFault fault) { editText_userInfo.setText(fault.toString()); } }); } private void scanDataFromQRCode(View view) { Intent intent = new Intent(ZXING_PACKAGE + ".SCAN"); intent.setPackage(ZXING_PACKAGE); intent.putExtra("SCAN_MODE", "QR_CODE_MODE"); startActivityForResult(intent, rc_ScanQR); } private void loginWithQRCode(String channelName) { Log.i(MainActivity.class.getSimpleName(), "loginWithQRCode: start remote login process"); if (userToken == null) { Log.i(TAG, "loginWithQRCode: userToken is null."); return; } Log.i(MainActivity.class.getSimpleName(), "loginWithQRCode [channelName=" + channelName + ", userToken=" + this.userToken + "]"); Backendless.Messaging.publish(channelName, this.userToken, new AsyncCallback<MessageStatus>() { @Override public void handleResponse(MessageStatus response) { Log.i(MainActivity.class.getSimpleName(), "loginWithQRCode: sent token successfully"); } @Override public void handleFault(BackendlessFault fault) { } }); this.channelName = null; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (resultCode == RESULT_OK) { String contents = intent.getStringExtra("SCAN_RESULT"); String format = intent.getStringExtra("SCAN_RESULT_FORMAT"); switch (requestCode) { case rc_ScanQR: this.channelName = contents; break; } } else if (resultCode == RESULT_CANCELED) { // Handle cancel } } private boolean isPackageInstalled(String packageName) { PackageManager pm = this.getApplicationContext().getPackageManager(); try { pm.getPackageInfo(packageName, 0); return true; } catch (PackageManager.NameNotFoundException e) { return false; } } private void installZXing(View view) { Intent googlePlayIntent = new Intent(Intent.ACTION_VIEW, zxingUri); googlePlayIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(googlePlayIntent); } }
Create a web application. In this case, it is a simple html file with some JavaScript code in it (you can find the complete file in the archive we shared with you). Again, please pay attention to the code highlighted in bold:
<!doctype html> <html lang="en"> <head> <title>Backendless QR Login Demo</title> <script type="text/javascript" src="//api.backendless.com/sdk/js/latest/backendless.js"></script> <style> .qr { width: 300px; height: 300px; text-align: center; vertical-align: middle; } #user_info { width: 600px; height: 100px; margin-top: 25px; margin-bottom: 25px; } </style> </head> <body> <h2>Backendless QR Login Demo</h2> <div id="user_info"> </div> <div id="qr_image" class="qr"> <img src="path" align="middle" alt="QR code."/> </div> <script> var APP_ID = YOUR-APP-ID; var API_KEY = YOUR-JS-API-KEY; Backendless.initApp( APP_ID, API_KEY ); var channel = null; var channelName = null; var filePath = null; function clear() { channel.leave(); channel = null; channelName = null; filePath = null; } function initializeQRLoginSession() { Backendless.LocalCache.set("user-token", null); if (channel != null) clear(); Backendless.CustomServices.invoke( "QRCodeGenerator", "generateQRCodeForLogin" ).then( function ( qrData ) { channelName = qrData.data; filePath = qrData.file; console.log( "channelName: ", channelName ); console.log( "filePath: ", filePath ); img = `<img src="${filePath}" alt="QR code" align="middle"/>`; qr_image.innerHTML = img; channel = Backendless.Messaging.subscribe( channelName ); channel.addMessageListener( onMessage ) } ); } function onMessage( data ) { console.log( 'got a message: ', data ); // channel.removeMessageListener( onMessage ); clear(); login(data.message); } async function login(token) { console.log("user-token: " + token); Backendless.LocalCache.set("user-token", token); const userObjectId = await Backendless.CustomServices.invoke( "QRCodeGenerator", "getUserId" ); console.log("current-user-id: " + userObjectId); Backendless.LocalCache.set("current-user-id", userObjectId); const userObject = await Backendless.Data.of(Backendless.User).findById( userObjectId ); user_info.innerHTML = JSON.stringify(userObject); qr_image.innerHTML = '' } initializeQRLoginSession(); </script> </body> </html>
The file must be uploaded to the Files section of your Backendless application:
Copy the link for the web application:
Open/paste the link in a web browser and press the “Login with a QR code” button. The web app fetches a QR code from the server and displays it.
Use the Android application to log in with your userid and password (make sure to create a user in your Backendless application). Once you log in, you will see the “Login with QR” button. Click the button to scan the QR code in the web app.
Your web application should log in automatically as soon as you scan the QR code in the Android app.
Hope you enjoyed this article. Happy coding!
Mobile-to-Web Cross Login Using a QR Code is difficult to explain. Backendless has done a tremendous job in providing a step by step approach towards solving the issue.