Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for auth emulator #142

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
24 changes: 16 additions & 8 deletions lib/auth/auth_gateway.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import 'dart:convert';

import 'package:firedart/auth/client.dart';
import 'package:firedart/auth/token_provider.dart';
import 'package:firedart/shared/emulator.dart';

import 'exceptions.dart';
import 'user_gateway.dart';

class AuthGateway {
final KeyClient client;
final TokenProvider tokenProvider;
final Emulator? emulator;

AuthGateway(this.client, this.tokenProvider);
AuthGateway(this.client, this.tokenProvider, {this.emulator});

Future<User> signUp(String email, String password) =>
_auth('signUp', {'email': email, 'password': password})
Expand All @@ -21,7 +23,7 @@ class AuthGateway {
.then(User.fromMap);

Future<void> signInWithCustomToken(String token) => _auth(
'signInWithCustomToken', {'token': token, 'returnSecureToken': 'true'});
'signInWithCustomToken', {'token': token, 'returnSecureToken': true});

Future<User> signInAnonymously() => _auth('signUp', {}).then(User.fromMap);

Expand All @@ -31,10 +33,10 @@ class AuthGateway {
});

Future<Map<String, dynamic>> _auth(
String method, Map<String, String> payload) async {
String method, Map<String, Object?> payload) async {
final body = {
...payload,
'returnSecureToken': 'true',
'returnSecureToken': true,
};

final map = await _post(method, body);
Expand All @@ -44,13 +46,19 @@ class AuthGateway {
}

Future<Map<String, dynamic>> _post(
String method, Map<String, String> body) async {
final requestUrl =
'https://identitytoolkit.googleapis.com/v1/accounts:$method';
String method, Map<String, Object?> body) async {
final requestPath = 'identitytoolkit.googleapis.com/v1/accounts:$method';

final requestUrl = emulator != null
? 'http://${emulator!.host}:${emulator!.port}/$requestPath'
: 'https://$requestPath';

final response = await client.post(
Uri.parse(requestUrl),
body: body,
body: json.encode(body),
headers: {
'content-type': 'application/json',
},
);

if (response.statusCode != 200) {
Expand Down
25 changes: 17 additions & 8 deletions lib/auth/client.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:convert';

import 'package:firedart/auth/token_provider.dart';
import 'package:firedart/shared/emulator.dart';
import 'package:http/http.dart' as http;

class VerboseClient extends http.BaseClient {
Expand Down Expand Up @@ -38,15 +39,23 @@ class VerboseClient extends http.BaseClient {
class KeyClient extends http.BaseClient {
final http.Client client;
final String apiKey;
final Emulator? emulator;

KeyClient(this.client, this.apiKey);
KeyClient(this.client, this.apiKey, {this.emulator});

@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
if (!request.url.queryParameters.containsKey('key')) {
var query = Map<String, String>.from(request.url.queryParameters)
..['key'] = apiKey;
var url = Uri.https(request.url.authority, request.url.path, query);

var url = emulator == null
? Uri.http(
request.url.authority,
request.url.path,
query,
)
: Uri.https(request.url.authority, request.url.path, query);
request = http.Request(request.method, url)
..headers.addAll(request.headers)
..bodyBytes = (request as http.Request).bodyBytes;
Expand All @@ -63,13 +72,13 @@ class UserClient extends http.BaseClient {

@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
var body = (request as http.Request).bodyFields;
var body = (request as http.Request).body;
request = http.Request(request.method, request.url)
..headers.addAll({
...request.headers,
'content-type': 'application/x-www-form-urlencoded'
})
..bodyFields = {...body, 'idToken': await tokenProvider.idToken};
..headers.addAll({...request.headers, 'content-type': 'application/json'})
..body = body.isNotEmpty
? json.encode(
{...json.decode(body), 'idToken': await tokenProvider.idToken})
: json.encode({'idToken': await tokenProvider.idToken});
return client.send(request);
}
}
19 changes: 12 additions & 7 deletions lib/auth/firebase_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ import 'package:firedart/auth/client.dart';
import 'package:firedart/auth/token_provider.dart';
import 'package:firedart/auth/token_store.dart';
import 'package:firedart/auth/user_gateway.dart';
import 'package:firedart/shared/emulator.dart';
import 'package:http/http.dart' as http;

class FirebaseAuth {
/* Singleton interface */
static FirebaseAuth? _instance;

static FirebaseAuth initialize(String apiKey, TokenStore tokenStore,
{http.Client? httpClient}) {
{http.Client? httpClient, Emulator? emulator}) {
if (initialized) {
throw Exception('FirebaseAuth instance was already initialized');
}
_instance = FirebaseAuth(apiKey, tokenStore, httpClient: httpClient);
_instance = FirebaseAuth(apiKey, tokenStore,
httpClient: httpClient, emulator: emulator);
return _instance!;
}

Expand All @@ -31,20 +33,23 @@ class FirebaseAuth {
/* Instance interface */
final String apiKey;

final Emulator? emulator;

http.Client httpClient;
late TokenProvider tokenProvider;

late AuthGateway _authGateway;
late UserGateway _userGateway;

FirebaseAuth(this.apiKey, TokenStore tokenStore, {http.Client? httpClient})
FirebaseAuth(this.apiKey, TokenStore tokenStore,
{http.Client? httpClient, this.emulator})
: assert(apiKey.isNotEmpty),
httpClient = httpClient ?? http.Client() {
var keyClient = KeyClient(this.httpClient, apiKey);
tokenProvider = TokenProvider(keyClient, tokenStore);
var keyClient = KeyClient(this.httpClient, apiKey, emulator: emulator);
tokenProvider = TokenProvider(keyClient, tokenStore, emulator: emulator);

_authGateway = AuthGateway(keyClient, tokenProvider);
_userGateway = UserGateway(keyClient, tokenProvider);
_authGateway = AuthGateway(keyClient, tokenProvider, emulator: emulator);
_userGateway = UserGateway(keyClient, tokenProvider, emulator: emulator);
}

bool get isSignedIn => tokenProvider.isSignedIn;
Expand Down
9 changes: 7 additions & 2 deletions lib/auth/token_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:convert';

import 'package:firedart/auth/client.dart';
import 'package:firedart/auth/token_store.dart';
import 'package:firedart/shared/emulator.dart';

import 'exceptions.dart';

Expand All @@ -11,10 +12,11 @@ const _tokenExpirationThreshold = Duration(seconds: 30);
class TokenProvider {
final KeyClient client;
final TokenStore _tokenStore;
final Emulator? emulator;

final StreamController<bool> _signInStateStreamController;

TokenProvider(this.client, this._tokenStore)
TokenProvider(this.client, this._tokenStore, {this.emulator})
: _signInStateStreamController = StreamController<bool>();

String? get userId => _tokenStore.userId;
Expand Down Expand Up @@ -53,7 +55,10 @@ class TokenProvider {

Future _refresh() async {
var response = await client.post(
Uri.parse('https://securetoken.googleapis.com/v1/token'),
emulator != null
? Uri.parse(
'http://${emulator!.host}:${emulator!.port}/securetoken.googleapis.com/v1/token')
: Uri.parse('https://securetoken.googleapis.com/v1/token'),
body: {
'grant_type': 'refresh_token',
'refresh_token': _tokenStore.refreshToken,
Expand Down
10 changes: 7 additions & 3 deletions lib/auth/user_gateway.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import 'dart:convert';

import 'package:firedart/auth/client.dart';
import 'package:firedart/auth/token_provider.dart';
import 'package:firedart/shared/emulator.dart';

class UserGateway {
final UserClient _client;
final Emulator? emulator;

UserGateway(KeyClient client, TokenProvider tokenProvider)
UserGateway(KeyClient client, TokenProvider tokenProvider, {this.emulator})
: _client = UserClient(client, tokenProvider);

Future<void> requestEmailVerification({String? langCode}) => _post(
Expand Down Expand Up @@ -40,8 +42,10 @@ class UserGateway {

Future<Map<String, dynamic>> _post<T>(String method, Map<String, String> body,
{Map<String, String>? headers}) async {
var requestUrl =
'https://identitytoolkit.googleapis.com/v1/accounts:$method';
final requestPath = 'identitytoolkit.googleapis.com/v1/accounts:$method';
final requestUrl = emulator != null
? 'http://${emulator!.host}:${emulator!.port}/$requestPath'
: 'https://$requestPath';

var response = await _client.post(
Uri.parse(requestUrl),
Expand Down
1 change: 1 addition & 0 deletions lib/firedart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export 'package:firedart/auth/firebase_auth.dart';
export 'package:firedart/auth/token_store.dart';
export 'package:firedart/firestore/firestore.dart';
export 'package:firedart/firestore/models.dart';
export 'package:firedart/shared/emulator.dart';
export 'package:grpc/grpc.dart' show GrpcError;
7 changes: 4 additions & 3 deletions lib/firestore/application_default_authenticator.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import 'package:firedart/shared/emulator.dart';
import 'package:grpc/grpc.dart';

class ApplicationDefaultAuthenticator {
ApplicationDefaultAuthenticator({required this.useEmulator});
ApplicationDefaultAuthenticator({this.emulator});

final bool useEmulator;
final Emulator? emulator;

late final Future<HttpBasedAuthenticator> _delegate =
applicationDefaultCredentialsAuthenticator([
'https://www.googleapis.com/auth/datastore',
]);

Future<void> authenticate(Map<String, String> metadata, String uri) async {
if (useEmulator) {
if (emulator != null) {
metadata['authorization'] = 'Bearer owner';

return;
Expand Down
10 changes: 2 additions & 8 deletions lib/firestore/firestore.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import 'package:firedart/auth/firebase_auth.dart';
import 'package:firedart/firestore/application_default_authenticator.dart';
import 'package:firedart/firestore/token_authenticator.dart';
import 'package:firedart/shared/emulator.dart';

import 'firestore_gateway.dart';
import 'models.dart';

class Emulator {
Emulator(this.host, this.port);

final String host;
final int port;
}

class Firestore {
/* Singleton interface */
static Firestore? _instance;
Expand All @@ -28,7 +22,7 @@ class Firestore {
final RequestAuthenticator? authenticator;
if (useApplicationDefaultAuth) {
authenticator = ApplicationDefaultAuthenticator(
useEmulator: emulator != null,
emulator: emulator,
).authenticate;
} else {
FirebaseAuth? auth;
Expand Down
22 changes: 11 additions & 11 deletions lib/firestore/firestore_gateway.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:firedart/generated/google/firestore/v1/common.pb.dart';
import 'package:firedart/generated/google/firestore/v1/document.pb.dart' as fs;
import 'package:firedart/generated/google/firestore/v1/firestore.pbgrpc.dart';
import 'package:firedart/generated/google/firestore/v1/query.pb.dart';
import 'package:firedart/shared/emulator.dart';
import 'package:grpc/grpc.dart';

import '../firedart.dart';
Expand All @@ -22,12 +23,11 @@ class FirestoreGateway {

late ClientChannel _channel;

FirestoreGateway(
String projectId, {
String? databaseId,
RequestAuthenticator? authenticator,
Emulator? emulator,
}) : _authenticator = authenticator,
FirestoreGateway(String projectId,
{String? databaseId,
RequestAuthenticator? authenticator,
Emulator? emulator})
: _authenticator = authenticator,
database =
'projects/$projectId/databases/${databaseId ?? '(default)'}/documents',
_listenStreamCache = <String, _ListenStreamWrapper>{} {
Expand Down Expand Up @@ -161,17 +161,17 @@ class FirestoreGateway {
? CallOptions(providers: [_authenticator!])
: null;
_listenStreamCache.clear();
_channel = emulator == null
_channel = emulator != null
? ClientChannel(
'firestore.googleapis.com',
options: ChannelOptions(),
)
: ClientChannel(
emulator.host,
port: emulator.port,
options: ChannelOptions(
credentials: ChannelCredentials.insecure(),
),
)
: ClientChannel(
'firestore.googleapis.com',
options: ChannelOptions(),
);
_client = FirestoreClient(
_channel,
Expand Down
6 changes: 6 additions & 0 deletions lib/shared/emulator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// An [Emulator] provides the [host] and [port] for a Firebase emulator.
class Emulator {
Emulator({required this.host, required this.port});
final String host;
final int port;
}
5 changes: 4 additions & 1 deletion test/firestore_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:io';
import 'package:firedart/firedart.dart';
import 'package:firedart/firestore/application_default_authenticator.dart';
import 'package:firedart/firestore/token_authenticator.dart';
import 'package:firedart/shared/emulator.dart';
import 'package:test/test.dart';

import 'firebase_auth_test.dart';
Expand Down Expand Up @@ -59,7 +60,9 @@ Future main() async {
'See the docs: https://cloud.google.com/docs/authentication/application-default-credentials#GAC',
);

final auth = ApplicationDefaultAuthenticator(useEmulator: false);
final auth = ApplicationDefaultAuthenticator(
emulator: Emulator(host: 'localhost', port: 4000),
);
var firestore = Firestore(projectId, authenticator: auth.authenticate);

runTests(firestore);
Expand Down