Implement native web auth

This commit is contained in:
Igor Kulikov
2021-06-17 10:29:52 +03:00
parent 8b3a175ead
commit 33562eae89
12 changed files with 236 additions and 13 deletions

View File

@@ -48,6 +48,10 @@ android {
signingConfig signingConfigs.debug
}
}
dependencies {
implementation 'androidx.browser:browser:1.0.0'
}
}
flutter {

View File

@@ -45,8 +45,8 @@
</intent-filter>
</activity>
<activity android:name="com.linusu.flutter_web_auth.CallbackActivity" >
<intent-filter android:label="flutter_web_auth">
<activity android:name=".TbWebCallbackActivity" >
<intent-filter android:label="tb_web_auth">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

View File

@@ -0,0 +1,16 @@
package org.thingsboard.app
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
class KeepAliveService: Service() {
companion object {
val binder = Binder()
}
override fun onBind(intent: Intent): IBinder {
return binder
}
}

View File

@@ -1,6 +1,20 @@
package org.thingsboard.app
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
registerTbWebAuth(flutterEngine)
}
fun registerTbWebAuth(flutterEngine: FlutterEngine) {
val channel = MethodChannel(flutterEngine.dartExecutor, "tb_web_auth")
channel.setMethodCallHandler(TbWebAuthHandler(this))
}
}

View File

@@ -0,0 +1,50 @@
package org.thingsboard.app
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry.Registrar
class TbWebAuthHandler(private val context: Context): MethodCallHandler {
companion object {
val callbacks = mutableMapOf<String, Result>()
}
override fun onMethodCall(call: MethodCall, resultCallback: Result) {
when (call.method) {
"authenticate" -> {
val url = Uri.parse(call.argument("url"))
val callbackUrlScheme = call.argument<String>("callbackUrlScheme")!!
val saveHistory = call.argument<Boolean>("saveHistory")
callbacks[callbackUrlScheme] = resultCallback
val intent = CustomTabsIntent.Builder().build()
val keepAliveIntent = Intent(context, KeepAliveService::class.java)
intent.intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
if (saveHistory != null && !saveHistory) {
intent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
}
intent.intent.putExtra("android.support.customtabs.extra.KEEP_ALIVE", keepAliveIntent)
intent.launchUrl(context, url)
}
"cleanUpDanglingCalls" -> {
callbacks.forEach{ (_, danglingResultCallback) ->
danglingResultCallback.error("CANCELED", "User canceled login", null)
}
callbacks.clear()
resultCallback.success(null)
}
else -> resultCallback.notImplemented()
}
}
}

View File

@@ -0,0 +1,20 @@
package org.thingsboard.app
import android.app.Activity
import android.net.Uri
import android.os.Bundle
class TbWebCallbackActivity: Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val url = intent?.data
val scheme = url?.scheme
if (scheme != null) {
TbWebAuthHandler.callbacks.remove(scheme)?.success(url.toString())
}
finish()
}
}

View File

@@ -7,7 +7,20 @@ import Flutter
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
self?.registerTbWebAuth()
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func registerTbWebAuth() {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: "tb_web_auth", binaryMessenger: controller.binaryMessenger)
let instance = TbWebAuthHandler()
channel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
instance.handle(call: call, result: result)
})
}
}

View File

@@ -0,0 +1,74 @@
import AuthenticationServices
import SafariServices
import Flutter
import UIKit
public class TbWebAuthHandler: NSObject {
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if call.method == "authenticate" {
let url = URL(string: (call.arguments as! Dictionary<String, AnyObject>)["url"] as! String)!
let callbackURLScheme = (call.arguments as! Dictionary<String, AnyObject>)["callbackUrlScheme"] as! String
var sessionToKeepAlive: Any? = nil // if we do not keep the session alive, it will get closed immediately while showing the dialog
let completionHandler = { (url: URL?, err: Error?) in
sessionToKeepAlive = nil
if let err = err {
if #available(iOS 12, *) {
if case ASWebAuthenticationSessionError.canceledLogin = err {
result(FlutterError(code: "CANCELED", message: "User canceled login", details: nil))
return
}
}
if #available(iOS 11, *) {
if case SFAuthenticationError.canceledLogin = err {
result(FlutterError(code: "CANCELED", message: "User canceled login", details: nil))
return
}
}
result(FlutterError(code: "EUNKNOWN", message: err.localizedDescription, details: nil))
return
}
result(url!.absoluteString)
}
if #available(iOS 12, *) {
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme, completionHandler: completionHandler)
if #available(iOS 13, *) {
guard let provider = UIApplication.shared.delegate?.window??.rootViewController as? FlutterViewController else {
result(FlutterError(code: "FAILED", message: "Failed to aquire root FlutterViewController" , details: nil))
return
}
session.presentationContextProvider = provider
}
session.start()
sessionToKeepAlive = session
} else if #available(iOS 11, *) {
let session = SFAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme, completionHandler: completionHandler)
session.start()
sessionToKeepAlive = session
} else {
result(FlutterError(code: "FAILED", message: "This plugin does currently not support iOS lower than iOS 11" , details: nil))
}
} else if (call.method == "cleanUpDanglingCalls") {
// we do not keep track of old callbacks on iOS, so nothing to do here
result(nil)
} else {
result(FlutterMethodNotImplemented)
}
}
}
@available(iOS 13, *)
extension FlutterViewController: ASWebAuthenticationPresentationContextProviding {
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return self.view.window!
}
}

View File

@@ -1,8 +1,8 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_web_auth/flutter_web_auth.dart';
import 'package:thingsboard_app/constants/app_constants.dart';
import 'package:thingsboard_app/core/auth/web/tb_web_auth.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:crypto/crypto.dart';
@@ -49,9 +49,9 @@ class TbOAuth2Client {
params['pkg'] = pkgName;
params['appToken'] = appToken;
url = url.replace(queryParameters: params);
final result = await FlutterWebAuth.authenticate(
final result = await TbWebAuth.authenticate(
url: url.toString(),
callbackUrlScheme: ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme);
callbackUrlScheme: ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme, saveHistory: false);
final resultUri = Uri.parse(result);
final error = resultUri.queryParameters['error'];
if (error != null) {

View File

@@ -0,0 +1,40 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart' show MethodChannel;
class _OnAppLifecycleResumeObserver extends WidgetsBindingObserver {
final Function onResumed;
_OnAppLifecycleResumeObserver(this.onResumed);
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
onResumed();
}
}
}
class TbWebAuth {
static const MethodChannel _channel = const MethodChannel('tb_web_auth');
static final _OnAppLifecycleResumeObserver _resumedObserver = _OnAppLifecycleResumeObserver(() {
_cleanUpDanglingCalls();
});
static Future<String> authenticate({required String url, required String callbackUrlScheme, bool? saveHistory}) async {
WidgetsBinding.instance?.removeObserver(_resumedObserver); // safety measure so we never add this observer twice
WidgetsBinding.instance?.addObserver(_resumedObserver);
return await _channel.invokeMethod('authenticate', <String, dynamic>{
'url': url,
'callbackUrlScheme': callbackUrlScheme,
'saveHistory': saveHistory,
}) as String;
}
static Future<void> _cleanUpDanglingCalls() async {
await _channel.invokeMethod('cleanUpDanglingCalls');
WidgetsBinding.instance?.removeObserver(_resumedObserver);
}
}

View File

@@ -198,13 +198,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_auth:
dependency: "direct main"
description:
name: flutter_web_auth
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
flutter_web_plugins:
dependency: transitive
description: flutter

View File

@@ -35,7 +35,6 @@ dependencies:
device_info: ^2.0.0
geolocator: ^7.0.3
material_design_icons_flutter: ^5.0.5955-rc.1
flutter_web_auth: ^0.3.0
package_info: ^2.0.2
dart_jsonwebtoken: ^2.2.0
crypto: ^3.0.1