Implement native web auth
This commit is contained in:
@@ -48,6 +48,10 @@ android {
|
|||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.browser:browser:1.0.0'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
|||||||
@@ -45,8 +45,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name="com.linusu.flutter_web_auth.CallbackActivity" >
|
<activity android:name=".TbWebCallbackActivity" >
|
||||||
<intent-filter android:label="flutter_web_auth">
|
<intent-filter android:label="tb_web_auth">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<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
|
package org.thingsboard.app
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
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,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
|
||||||
|
self?.registerTbWebAuth()
|
||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
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:convert';
|
||||||
import 'dart:typed_data';
|
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/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:thingsboard_app/core/context/tb_context.dart';
|
||||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
@@ -49,9 +49,9 @@ class TbOAuth2Client {
|
|||||||
params['pkg'] = pkgName;
|
params['pkg'] = pkgName;
|
||||||
params['appToken'] = appToken;
|
params['appToken'] = appToken;
|
||||||
url = url.replace(queryParameters: params);
|
url = url.replace(queryParameters: params);
|
||||||
final result = await FlutterWebAuth.authenticate(
|
final result = await TbWebAuth.authenticate(
|
||||||
url: url.toString(),
|
url: url.toString(),
|
||||||
callbackUrlScheme: ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme);
|
callbackUrlScheme: ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme, saveHistory: false);
|
||||||
final resultUri = Uri.parse(result);
|
final resultUri = Uri.parse(result);
|
||||||
final error = resultUri.queryParameters['error'];
|
final error = resultUri.queryParameters['error'];
|
||||||
if (error != null) {
|
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
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ dependencies:
|
|||||||
device_info: ^2.0.0
|
device_info: ^2.0.0
|
||||||
geolocator: ^7.0.3
|
geolocator: ^7.0.3
|
||||||
material_design_icons_flutter: ^5.0.5955-rc.1
|
material_design_icons_flutter: ^5.0.5955-rc.1
|
||||||
flutter_web_auth: ^0.3.0
|
|
||||||
package_info: ^2.0.2
|
package_info: ^2.0.2
|
||||||
dart_jsonwebtoken: ^2.2.0
|
dart_jsonwebtoken: ^2.2.0
|
||||||
crypto: ^3.0.1
|
crypto: ^3.0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user