diff --git a/.github/workflows/deploy_app_dev.yaml b/.github/workflows/deploy_app_dev.yaml index 288ff5b0b..8fa0c9c83 100644 --- a/.github/workflows/deploy_app_dev.yaml +++ b/.github/workflows/deploy_app_dev.yaml @@ -15,7 +15,7 @@ jobs: with: channel: "stable" - run: flutter packages get - - run: flutter build web --web-renderer canvaskit -t lib/main_development.dart + - run: flutter build web --web-renderer canvaskit -t lib/main_development.dart --dart-define RECAPTCHA_KEY=${{ secrets.RECAPTCHA_KEY }} - uses: FirebaseExtended/action-hosting-deploy@v0 diff --git a/.github/workflows/deploy_app_prod.yaml b/.github/workflows/deploy_app_prod.yaml index bf09f6b35..071dbd192 100644 --- a/.github/workflows/deploy_app_prod.yaml +++ b/.github/workflows/deploy_app_prod.yaml @@ -12,7 +12,7 @@ jobs: with: channel: "stable" - run: flutter packages get - - run: flutter build web --web-renderer canvaskit -t lib/main_production.dart + - run: flutter build web --web-renderer canvaskit -t lib/main_production.dart --dart-define RECAPTCHA_KEY=${{ secrets.RECAPTCHA_KEY_PROD }} - uses: FirebaseExtended/action-hosting-deploy@v0 diff --git a/.github/workflows/deploy_app_staging.yaml b/.github/workflows/deploy_app_staging.yaml index cd31932fa..90d3f35d0 100644 --- a/.github/workflows/deploy_app_staging.yaml +++ b/.github/workflows/deploy_app_staging.yaml @@ -12,7 +12,7 @@ jobs: with: channel: "stable" - run: flutter packages get - - run: flutter build web --web-renderer canvaskit -t lib/main_staging.dart + - run: flutter build web --web-renderer canvaskit -t lib/main_staging.dart --dart-define RECAPTCHA_KEY=${{ secrets.RECAPTCHA_KEY }} - uses: FirebaseExtended/action-hosting-deploy@v0 diff --git a/api/headers/allow_headers.dart b/api/headers/allow_headers.dart new file mode 100644 index 000000000..80340b172 --- /dev/null +++ b/api/headers/allow_headers.dart @@ -0,0 +1,21 @@ +import 'package:dart_frog/dart_frog.dart'; +import 'package:jwt_middleware/jwt_middleware.dart'; +import 'package:shelf_cors_headers/shelf_cors_headers.dart'; + +Middleware allowHeaders() { + return (handler) { + return (context) async { + final response = await handler(context); + final headers = Map.from(response.headers); + final accessControlAllowHeaders = headers[ACCESS_CONTROL_ALLOW_HEADERS]; + if (accessControlAllowHeaders != null) { + headers[ACCESS_CONTROL_ALLOW_HEADERS] = + '$accessControlAllowHeaders, $X_FIREBASE_APPCHECK'; + + return response.copyWith(headers: headers); + } + + return response; + }; + }; +} diff --git a/api/headers/headers.dart b/api/headers/headers.dart new file mode 100644 index 000000000..4063a2af8 --- /dev/null +++ b/api/headers/headers.dart @@ -0,0 +1,2 @@ +export 'allow_headers.dart'; +export 'cors_headers.dart'; diff --git a/api/routes/_middleware.dart b/api/routes/_middleware.dart index f6f5eb4c6..ef7af7dd8 100644 --- a/api/routes/_middleware.dart +++ b/api/routes/_middleware.dart @@ -6,7 +6,7 @@ import 'package:google_cloud/google_cloud.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:logging/logging.dart'; -import '../headers/cors_headers.dart'; +import '../headers/headers.dart'; import '../main.dart'; Handler middleware(Handler handler) { @@ -18,5 +18,6 @@ Handler middleware(Handler handler) { .use(provider((_) => leaderboardRepository)) .use(provider((_) => firebaseCloudStorage)) .use(corsHeaders()) + .use(allowHeaders()) .use(fromShelfMiddleware(cloudLoggingMiddleware(projectId))); } diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 08bc81ad1..1c246d651 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:bloc/bloc.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/widgets.dart'; @@ -25,6 +26,7 @@ class AppBlocObserver extends BlocObserver { typedef BootstrapBuilder = FutureOr Function( FirebaseFirestore firestore, FirebaseAuth firebaseAuth, + FirebaseAppCheck appCheck, ); Future bootstrap(BootstrapBuilder builder) async { @@ -40,6 +42,7 @@ Future bootstrap(BootstrapBuilder builder) async { await builder( FirebaseFirestore.instance, FirebaseAuth.instance, + FirebaseAppCheck.instance, ), ); } diff --git a/lib/main_debug.dart b/lib/main_debug.dart index 660b5cb8d..3b0ca0417 100644 --- a/lib/main_debug.dart +++ b/lib/main_debug.dart @@ -1,10 +1,15 @@ +// ignore_for_file: avoid_web_libraries_in_flutter + import 'dart:async'; +import 'dart:js' as js; import 'package:api_client/api_client.dart'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:board_info_repository/board_info_repository.dart'; import 'package:crossword_repository/crossword_repository.dart'; +import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:io_crossword/app/app.dart'; import 'package:io_crossword/bootstrap.dart'; @@ -12,6 +17,11 @@ import 'package:io_crossword/crossword/game/game.dart'; import 'package:io_crossword/firebase_options_development.dart'; void main() async { + if (kDebugMode) { + js.context['FIREBASE_APPCHECK_DEBUG_TOKEN'] = + const String.fromEnvironment('APPCHECK_DEBUG_TOKEN'); + } + WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( @@ -20,7 +30,14 @@ void main() async { unawaited( bootstrap( - (firestore, firebaseAuth) async { + (firestore, firebaseAuth, appCheck) async { + await appCheck.activate( + webProvider: ReCaptchaV3Provider( + const String.fromEnvironment('RECAPTCHA_KEY'), + ), + ); + await appCheck.setTokenAutoRefreshEnabled(true); + final authenticationRepository = AuthenticationRepository( firebaseAuth: firebaseAuth, ); @@ -34,8 +51,8 @@ void main() async { baseUrl: 'https://io-crossword-dev-api-sea6y22h5q-uc.a.run.app', idTokenStream: authenticationRepository.idToken, refreshIdToken: authenticationRepository.refreshIdToken, - // TODO(any): implement app check - appCheckTokenStream: const Stream.empty(), + appCheckTokenStream: appCheck.onTokenChange, + appCheckToken: await appCheck.getToken(), ); return App( diff --git a/lib/main_development.dart b/lib/main_development.dart index f6161e892..6ba325d91 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -1,16 +1,26 @@ +// ignore_for_file: avoid_web_libraries_in_flutter + import 'dart:async'; +import 'dart:js' as js; import 'package:api_client/api_client.dart'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:board_info_repository/board_info_repository.dart'; import 'package:crossword_repository/crossword_repository.dart'; +import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:io_crossword/app/app.dart'; import 'package:io_crossword/bootstrap.dart'; import 'package:io_crossword/firebase_options_development.dart'; void main() async { + if (kDebugMode) { + js.context['FIREBASE_APPCHECK_DEBUG_TOKEN'] = + const String.fromEnvironment('APPCHECK_DEBUG_TOKEN'); + } + WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( @@ -19,7 +29,14 @@ void main() async { unawaited( bootstrap( - (firestore, firebaseAuth) async { + (firestore, firebaseAuth, appCheck) async { + await appCheck.activate( + webProvider: ReCaptchaV3Provider( + const String.fromEnvironment('RECAPTCHA_KEY'), + ), + ); + await appCheck.setTokenAutoRefreshEnabled(true); + final authenticationRepository = AuthenticationRepository( firebaseAuth: firebaseAuth, ); @@ -31,8 +48,8 @@ void main() async { baseUrl: 'https://io-crossword-dev-api-sea6y22h5q-uc.a.run.app', idTokenStream: authenticationRepository.idToken, refreshIdToken: authenticationRepository.refreshIdToken, - // TODO(any): implement app check - appCheckTokenStream: const Stream.empty(), + appCheckTokenStream: appCheck.onTokenChange, + appCheckToken: await appCheck.getToken(), ); return App( diff --git a/lib/main_local.dart b/lib/main_local.dart index 1d1874703..62919b5b2 100644 --- a/lib/main_local.dart +++ b/lib/main_local.dart @@ -1,9 +1,13 @@ +// ignore_for_file: avoid_web_libraries_in_flutter + import 'dart:async'; +import 'dart:js' as js; import 'package:api_client/api_client.dart'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:board_info_repository/board_info_repository.dart'; import 'package:crossword_repository/crossword_repository.dart'; +import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/widgets.dart'; import 'package:io_crossword/app/app.dart'; @@ -11,6 +15,9 @@ import 'package:io_crossword/bootstrap.dart'; import 'package:io_crossword/firebase_options_development.dart'; void main() async { + js.context['FIREBASE_APPCHECK_DEBUG_TOKEN'] = + const String.fromEnvironment('APPCHECK_DEBUG_TOKEN'); + WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( @@ -19,7 +26,14 @@ void main() async { unawaited( bootstrap( - (firestore, firebaseAuth) async { + (firestore, firebaseAuth, appCheck) async { + await appCheck.activate( + webProvider: ReCaptchaV3Provider( + const String.fromEnvironment('RECAPTCHA_KEY'), + ), + ); + await appCheck.setTokenAutoRefreshEnabled(true); + final authenticationRepository = AuthenticationRepository( firebaseAuth: firebaseAuth, ); @@ -31,8 +45,8 @@ void main() async { baseUrl: 'http://localhost:8080', idTokenStream: authenticationRepository.idToken, refreshIdToken: authenticationRepository.refreshIdToken, - // TODO(any): implement app check - appCheckTokenStream: const Stream.empty(), + appCheckTokenStream: appCheck.onTokenChange, + appCheckToken: await appCheck.getToken(), ); return App( diff --git a/lib/main_production.dart b/lib/main_production.dart index 7bf8412b5..495d6195d 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -4,6 +4,7 @@ import 'package:api_client/api_client.dart'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:board_info_repository/board_info_repository.dart'; import 'package:crossword_repository/crossword_repository.dart'; +import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/widgets.dart'; import 'package:io_crossword/app/app.dart'; @@ -19,7 +20,14 @@ void main() async { unawaited( bootstrap( - (firestore, firebaseAuth) async { + (firestore, firebaseAuth, appCheck) async { + await appCheck.activate( + webProvider: ReCaptchaV3Provider( + const String.fromEnvironment('RECAPTCHA_KEY'), + ), + ); + await appCheck.setTokenAutoRefreshEnabled(true); + final authenticationRepository = AuthenticationRepository( firebaseAuth: firebaseAuth, ); @@ -31,8 +39,8 @@ void main() async { baseUrl: 'https://io-crossword-api-u3emptgwka-uc.a.run.app', idTokenStream: authenticationRepository.idToken, refreshIdToken: authenticationRepository.refreshIdToken, - // TODO(any): implement app check - appCheckTokenStream: const Stream.empty(), + appCheckTokenStream: appCheck.onTokenChange, + appCheckToken: await appCheck.getToken(), ); return App( diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 257260034..7ac788969 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -1,16 +1,25 @@ +// ignore_for_file: avoid_web_libraries_in_flutter + import 'dart:async'; +import 'dart:js' as js; import 'package:api_client/api_client.dart'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:board_info_repository/board_info_repository.dart'; import 'package:crossword_repository/crossword_repository.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:io_crossword/app/app.dart'; import 'package:io_crossword/bootstrap.dart'; import 'package:io_crossword/firebase_options_development.dart'; void main() async { + if (kDebugMode) { + js.context['FIREBASE_APPCHECK_DEBUG_TOKEN'] = + const String.fromEnvironment('APPCHECK_DEBUG_TOKEN'); + } + WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( @@ -19,7 +28,7 @@ void main() async { unawaited( bootstrap( - (firestore, firebaseAuth) async { + (firestore, firebaseAuth, appCheck) async { final authenticationRepository = AuthenticationRepository( firebaseAuth: firebaseAuth, ); @@ -31,8 +40,8 @@ void main() async { baseUrl: 'https://io-crossword-staging-api-sea6y22h5q-uc.a.run.app', idTokenStream: authenticationRepository.idToken, refreshIdToken: authenticationRepository.refreshIdToken, - // TODO(any): implement app check - appCheckTokenStream: const Stream.empty(), + appCheckTokenStream: appCheck.onTokenChange, + appCheckToken: await appCheck.getToken(), ); return App( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 30cab9154..186d8e3c3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import audioplayers_darwin import cloud_firestore +import firebase_app_check import firebase_auth import firebase_core import path_provider_foundation @@ -15,6 +16,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 96d839228..25a26ed47 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -277,6 +277,30 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + firebase_app_check: + dependency: "direct main" + description: + name: firebase_app_check + sha256: db93b6dfc432d17c0c9a4b7917b6b2af103d74e6e830326fa8502b945cc7f139 + url: "https://pub.dev" + source: hosted + version: "0.2.1+19" + firebase_app_check_platform_interface: + dependency: transitive + description: + name: firebase_app_check_platform_interface + sha256: c7c8647385a9fbeade985f9542c6967f36a77984e7c7355bdb2c14b779518eed + url: "https://pub.dev" + source: hosted + version: "0.1.0+21" + firebase_app_check_web: + dependency: transitive + description: + name: firebase_app_check_web + sha256: "23d906eab0a54bbbf6bad58c3ee5b9461dfde0bd62896a8a60f50fb1cdcecc6a" + url: "https://pub.dev" + source: hosted + version: "0.1.1+1" firebase_auth: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f1acf3fd7..aa213644a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: crossword_repository: path: packages/crossword_repository equatable: ^2.0.5 + firebase_app_check: ^0.2.1+18 firebase_auth: ^4.18.0 firebase_core: ^2.27.1 flame: ^1.16.0