Implement native web auth
This commit is contained in:
@@ -48,6 +48,10 @@ android {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.browser:browser:1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
74
ios/Runner/TbWebAuthHandler.swift
Normal file
74
ios/Runner/TbWebAuthHandler.swift
Normal 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!
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
40
lib/core/auth/web/tb_web_auth.dart
Normal file
40
lib/core/auth/web/tb_web_auth.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user