🧱 OpenID Connect client infra

This commit is contained in:
LittleSheep 2025-06-16 01:44:24 +08:00
parent 4b9c9aec92
commit 4dbee27718
4 changed files with 176 additions and 0 deletions

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -6,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart'; import 'package:island/models/auth.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/account/me/settings.dart'; import 'package:island/screens/account/me/settings.dart';
import 'package:island/screens/auth/oidc.native.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
@ -159,6 +161,15 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
case 'google': case 'google':
case 'github': case 'github':
case 'discord': case 'discord':
final token = await Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => OidcScreen(
provider: selectedProvider.value.toLowerCase(),
),
),
);
print(token);
break; break;
default: default:
showSnackBar(context, 'accountConnectionAddError'.tr()); showSnackBar(context, 'accountConnectionAddError'.tr());

View File

@ -0,0 +1 @@
export 'oidc.native.dart' if (dart.library.html) 'oidc.web.dart';

View File

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:island/pods/config.dart';
import 'package:island/widgets/app_scaffold.dart';
class OidcScreen extends ConsumerStatefulWidget {
final String provider;
const OidcScreen({super.key, required this.provider});
@override
ConsumerState<OidcScreen> createState() => _OIDCScreenState();
}
class _OIDCScreenState extends ConsumerState<OidcScreen> {
InAppWebViewController? _webViewController;
String? authToken;
@override
Widget build(BuildContext context) {
final serverUrl = ref.watch(serverUrlProvider);
return AppScaffold(
appBar: AppBar(title: Text('login').tr()),
body: InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri('$serverUrl/auth/login/${widget.provider}'),
),
onWebViewCreated: (controller) {
_webViewController = controller;
// Register a handler to receive the token from JavaScript
controller.addJavaScriptHandler(
handlerName: 'tokenHandler',
callback: (args) {
// args[0] will be the token string
if (args.isNotEmpty && args[0] is String) {
setState(() {
authToken = args[0];
});
// Return the token and close the webview
Navigator.of(context).pop(authToken);
}
},
);
},
shouldOverrideUrlLoading: (controller, navigationAction) async {
final url = navigationAction.request.url;
if (url != null) {
final path = url.path;
final queryParams = url.queryParameters;
// Check if we're on the token page
if (path.contains('/auth/token') &&
queryParams.containsKey('token')) {
// Extract token from URL
final token = queryParams['token']!;
// Return the token and close the webview
Navigator.of(context).pop(token);
return NavigationActionPolicy.CANCEL;
}
}
return NavigationActionPolicy.ALLOW;
},
onLoadStop: (controller, url) async {
if (url != null && url.path.contains('/auth/token')) {
// Inject JavaScript to call our handler with the token
await controller.evaluateJavascript(
source: '''
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
window.flutter_inappwebview.callHandler('tokenHandler', token);
}
''',
);
}
},
),
);
}
}

View File

@ -0,0 +1,78 @@
// ignore_for_file: invalid_runtime_check_with_js_interop_types
import 'dart:ui_web' as ui;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:island/pods/config.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:web/web.dart' as web;
import 'package:flutter/material.dart';
class OidcScreen extends ConsumerStatefulWidget {
final String provider;
const OidcScreen({super.key, required this.provider});
@override
ConsumerState<OidcScreen> createState() => _OIDCScreenState();
}
class _OIDCScreenState extends ConsumerState<OidcScreen> {
bool _isInitialized = false;
final String _viewType = 'oidc-iframe';
void _setupWebListener(String serverUrl) {
// Listen for messages from the iframe
web.window.onMessage.listen((event) {
if (event.data != null && event.data is String) {
final message = event.data as String;
if (message.startsWith("token=")) {
String token = message.replaceFirst("token=", "");
// Return the token and close the screen
if (context.mounted) Navigator.pop(context, token);
}
}
});
// Create the iframe for the OIDC login
final iframe =
web.HTMLIFrameElement()
..src = '$serverUrl/auth/login/${widget.provider}'
..style.border = 'none'
..width = '100%'
..height = '100%';
// Add the iframe to the document body
web.document.body!.append(iframe);
// Register the iframe as a platform view
ui.platformViewRegistry.registerViewFactory(
_viewType,
(int viewId) => iframe,
);
setState(() {
_isInitialized = true;
});
}
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
final serverUrl = ref.watch(serverUrlProvider);
_setupWebListener(serverUrl);
});
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: Text('login').tr()),
body:
_isInitialized
? HtmlElementView(viewType: _viewType)
: Center(child: CircularProgressIndicator()),
);
}
}