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

Document the public API #140

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/auth/auth_gateway.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,35 @@ import 'package:firedart/auth/token_provider.dart';
import 'exceptions.dart';
import 'user_gateway.dart';

/// Keeps a [KeyClient] and [TokenProvider] to allow interacting with the
/// FirebaseAuth API.
///
/// On authenticating a [User] object is returned.
class AuthGateway {
final KeyClient client;
final TokenProvider tokenProvider;

AuthGateway(this.client, this.tokenProvider);

/// Create a new email and password user with the signupNewUser endpoint.
Future<User> signUp(String email, String password) =>
_auth('signUp', {'email': email, 'password': password})
.then(User.fromMap);

/// Sign in a user with an email and password using the verifyPassword endpoint.
Future<User> signIn(String email, String password) =>
_auth('signInWithPassword', {'email': email, 'password': password})
.then(User.fromMap);

/// Exchange a custom Auth token for an ID and refresh token using the
/// verifyCustomToken endpoint.
Future<void> signInWithCustomToken(String token) => _auth(
'signInWithCustomToken', {'token': token, 'returnSecureToken': 'true'});

/// Sign in a user anonymously using the signupNewUser endpoint.
Future<User> signInAnonymously() => _auth('signUp', {}).then(User.fromMap);

/// Apply a password reset change using the resetPassword endpoint.
Future<void> resetPassword(String email) => _post('sendOobCode', {
'requestType': 'PASSWORD_RESET',
'email': email,
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import 'dart:convert';
import 'package:firedart/auth/token_provider.dart';
import 'package:http/http.dart' as http;

/// Wraps a http client with print statements for debugging.
class VerboseClient extends http.BaseClient {
final http.Client _client;

VerboseClient() : _client = http.Client();

/// Send a http request, printing request details.
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
print('--> ${request.method} ${request.url}');
Expand Down Expand Up @@ -35,6 +37,7 @@ class VerboseClient extends http.BaseClient {
}
}

/// Wraps a http client and includes an API key for authenticated requests.
class KeyClient extends http.BaseClient {
final http.Client client;
final String apiKey;
Expand All @@ -55,6 +58,7 @@ class KeyClient extends http.BaseClient {
}
}

/// Wraps a http client and includes a [TokenProvider] for authenticated requests.
class UserClient extends http.BaseClient {
final KeyClient client;
final TokenProvider tokenProvider;
Expand Down
5 changes: 5 additions & 0 deletions lib/auth/exceptions.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import 'dart:convert';

/// An [Exception] that includes the body of a http request response.
class AuthException implements Exception {
final String body;

/// Extract the error message from the body.
String get message => jsonDecode(body)['error']['message'];

/// Extract the error code from the error message.
String get errorCode => message.split(' ')[0];

AuthException(this.body);
Expand All @@ -13,6 +16,8 @@ class AuthException implements Exception {
String toString() => 'AuthException: $errorCode';
}

/// An [Exception] thrown when an app attempted to make a Firestore request
/// before the user is signed in.
class SignedOutException implements Exception {
@override
String toString() =>
Expand Down
41 changes: 39 additions & 2 deletions lib/auth/firebase_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import 'package:firedart/auth/token_store.dart';
import 'package:firedart/auth/user_gateway.dart';
import 'package:http/http.dart' as http;

/// Keeps a [FirebaseAuth] singleton accessible via a global [instance] getter.
///
/// The singleton is initialized with an API key, a [TokenStore] and an optional
/// http client. A [KeyClient] is used if no http client is supplied.
class FirebaseAuth {
/* Singleton interface */
static FirebaseAuth? _instance;

/// Check if already initialized and if not, call the constructor.
static FirebaseAuth initialize(String apiKey, TokenStore tokenStore,
{http.Client? httpClient}) {
if (initialized) {
Expand All @@ -18,8 +22,10 @@ class FirebaseAuth {
return _instance!;
}

/// Global method to check if the singleton has been initialized.
static bool get initialized => _instance != null;

/// Global getter for accessing the [FirebaseAuth] singleton.
static FirebaseAuth get instance {
if (!initialized) {
throw Exception(
Expand All @@ -28,15 +34,27 @@ class FirebaseAuth {
return _instance!;
}

/* Instance interface */
/// The API key used to make authentication requests on a specific
/// Firebase/GCP project.
final String apiKey;

/// The http client used to make requests.
http.Client httpClient;

/// A [TokenProvider] that provides a [KeyClient] and [TokenStore].
late TokenProvider tokenProvider;

late AuthGateway _authGateway;
late UserGateway _userGateway;

/// A [FirebaseAuth] object is created with an API key for a GCP/Firebase
/// project, a [TokenStore] and optional http client.
///
/// A [KeyClient] is created from the http client that makes requests using
/// the given API key.
///
/// A [TokenProvider] is created from the [TokenStore] and provides the
/// signedIn state of the user.
FirebaseAuth(this.apiKey, TokenStore tokenStore, {http.Client? httpClient})
: assert(apiKey.isNotEmpty),
httpClient = httpClient ?? http.Client() {
Expand All @@ -49,41 +67,60 @@ class FirebaseAuth {

bool get isSignedIn => tokenProvider.isSignedIn;

/// Return a stream that is updated whenever the user's signed in state changes.
/// Does not automatically fire an event on the first listen of the stream.
Stream<bool> get signInState => tokenProvider.signInState;

/// Get the [userId] and throw if not signed in.
String get userId {
if (!isSignedIn) throw Exception('User signed out');
return tokenProvider.userId!;
}

/// Create a new email and password user with the signupNewUser endpoint.
Future<User> signUp(String email, String password) =>
_authGateway.signUp(email, password);

/// Sign in a user with an email and password using the verifyPassword endpoint.
Future<User> signIn(String email, String password) =>
_authGateway.signIn(email, password);

/// Exchange a custom Auth token for an ID and refresh token using the
/// verifyCustomToken endpoint.
Future<void> signInWithCustomToken(String token) =>
_authGateway.signInWithCustomToken(token);

/// Sign in a user anonymously using the signupNewUser endpoint.
Future<User> signInAnonymously() => _authGateway.signInAnonymously();

/// Clear the token store and notify listeners of the user's state change.
void signOut() => tokenProvider.signOut();

/// Close the http client.
void close() => httpClient.close();

/// Apply a password reset change using the resetPassword endpoint.
Future<void> resetPassword(String email) => _authGateway.resetPassword(email);

/// Send an email verification for the current user using the
/// getOobConfirmationCode endpoint.
Future<void> requestEmailVerification({String? langCode}) =>
_userGateway.requestEmailVerification(langCode: langCode);

/// Change a user's password using the setAccountInfo endpoint.
Future<void> changePassword(String password) =>
_userGateway.changePassword(password);

/// Get a user's data using the getAccountInfo endpoint and create a new
/// [User] from the returned account info.
Future<User> getUser() => _userGateway.getUser();

/// Update a user's profile (display name / photo URL) using the setAccountInfo
/// endpoint.
Future<void> updateProfile({String? displayName, String? photoUrl}) =>
_userGateway.updateProfile(displayName, photoUrl);

/// Delete a current user using the deleteAccount endpoint.
Future<void> deleteAccount() async {
await _userGateway.deleteAccount();
signOut();
Expand Down
10 changes: 10 additions & 0 deletions lib/auth/token_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import 'exceptions.dart';

const _tokenExpirationThreshold = Duration(seconds: 30);

/// A [TokenProvider] has a [KeyClient] for making requests using a project's
/// API key as well as a [TokenStore] for storing a [Token].
///
/// The [TokenProvider] also has getters for the [Token] members as well as
/// providing [signInState], a stream that emits booleans indicating whether or
/// not the user is currently signed in.
class TokenProvider {
final KeyClient client;
final TokenStore _tokenStore;
Expand All @@ -25,6 +31,7 @@ class TokenProvider {

Stream<bool> get signInState => _signInStateStreamController.stream;

/// Get an idToken, including refreshing the token if required.
Future<String> get idToken async {
if (!isSignedIn) throw SignedOutException();

Expand All @@ -36,6 +43,8 @@ class TokenProvider {
return _tokenStore.idToken!;
}

/// Set the [TokenStore]'s [Token] from a Map with keys value pairs for
/// the userId, the idToken and the refreshToken.
void setToken(Map<String, dynamic> map) {
_tokenStore.setToken(
map['localId'],
Expand All @@ -46,6 +55,7 @@ class TokenProvider {
_notifyState();
}

/// Clear the token store and notify listeners of the user's state change.
void signOut() {
_tokenStore.clear();
_notifyState();
Expand Down
11 changes: 11 additions & 0 deletions lib/auth/token_store.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/// A superclass for token stores requiring read and write methods to be
/// implemented, allowing for different strategies for token storage.
///
/// Consists of a [Token] with getters for an idToken for making
/// authenticated requests and a refreshToken for refreshing expired tokens,
/// as well as the expiry time in seconds.
abstract class TokenStore {
Token? _token;

Expand All @@ -11,13 +17,15 @@ abstract class TokenStore {

bool get hasToken => _token != null;

/// Create a [Token] from an [idToken], [refreshToken] and the time in seconds.
void setToken(
String? userId, String idToken, String refreshToken, int expiresIn) {
var expiry = DateTime.now().add(Duration(seconds: expiresIn));
_token = Token(userId, idToken, refreshToken, expiry);
write(_token);
}

/// Create a [TokenStore] using the [read] method to retrieve a token.
TokenStore() {
_token = read();
}
Expand Down Expand Up @@ -57,6 +65,9 @@ class VolatileStore extends TokenStore {
void delete() {}
}

/// Contains an idToken for making authenticated requests and a refreshToken
/// for refreshing expired tokens, as well as the expiry time of the idToken,
/// in seconds. Optionally holds the userId of the signed in user.
class Token {
final String? _userId;
final String _idToken;
Expand Down
17 changes: 17 additions & 0 deletions lib/auth/user_gateway.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,39 @@ import 'dart:convert';
import 'package:firedart/auth/client.dart';
import 'package:firedart/auth/token_provider.dart';

/// A [UserGateway] holds a UserClient that is made up of a [KeyClient] and a
/// [TokenProvider] allowing requests to the Firebase Auth API as well as
/// token storage for allowing authenticated requests.
class UserGateway {
final UserClient _client;

UserGateway(KeyClient client, TokenProvider tokenProvider)
: _client = UserClient(client, tokenProvider);

/// Send an email verification for the current user using the
/// getOobConfirmationCode endpoint.
Future<void> requestEmailVerification({String? langCode}) => _post(
'sendOobCode',
{'requestType': 'VERIFY_EMAIL'},
headers: {if (langCode != null) 'X-Firebase-Locale': langCode},
);

/// Get a user's data using the getAccountInfo endpoint and create a new
/// [User] from the returned account info.
Future<User> getUser() async {
var map = await _post('lookup', {});
return User.fromMap(map['users'][0]);
}

/// Change a user's password using the setAccountInfo endpoint.
Future<void> changePassword(String password) async {
await _post('update', {
'password': password,
});
}

/// Update a user's profile (display name / photo URL) using the setAccountInfo
/// endpoint.
Future<void> updateProfile(String? displayName, String? photoUrl) async {
assert(displayName != null || photoUrl != null);
await _post('update', {
Expand All @@ -34,6 +44,7 @@ class UserGateway {
});
}

/// Delete a current user using the deleteAccount endpoint.
Future<void> deleteAccount() async {
await _post('delete', {});
}
Expand All @@ -53,20 +64,26 @@ class UserGateway {
}
}

/// A [User] has a user id and optionally a [displayName], [photoUrl], [email]
/// and a boolean indicating whether or not the email has been verified.
class User {
final String id;
final String? displayName;
final String? photoUrl;
final String? email;
final bool? emailVerified;

/// Creates a [User] from a Map with key value pairs for the user id,
/// [displayName], [photoUrl], [email] and [emailVerified].
User.fromMap(Map<String, dynamic> map)
: id = map['localId'],
displayName = map['displayName'],
photoUrl = map['photoUrl'],
email = map['email'],
emailVerified = map['emailVerified'];

/// Convert the User to a Map with key value pairs for each of the members of
/// the [User] object.
Map<String, dynamic> toMap() => {
'localId': id,
'displayName': displayName,
Expand Down
20 changes: 20 additions & 0 deletions lib/firestore/application_default_authenticator.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import 'package:grpc/grpc.dart';

/// Application Default Credentials (ADC) is a strategy used by the authentication
/// libraries to automatically find credentials based on the application environment.
/// This means code can run in either a development or production environment
/// without changing how your application authenticates to googleapis.
///
/// [applicationDefaultCredentialsAuthenticator] looks for credentials in the
/// following order of preference:
/// 1. A JSON file whose path is specified by `GOOGLE_APPLICATION_CREDENTIALS`,
/// this file typically contains [exported service account keys][svc-keys].
/// 2. A JSON file created by [`gcloud auth application-default login`][gcloud-login]
/// in a well-known location (`%APPDATA%/gcloud/application_default_credentials.json`
/// on Windows and `$HOME/.config/gcloud/application_default_credentials.json` on Linux/Mac).
/// 3. On Google Compute Engine and App Engine Flex we fetch credentials from
/// [GCE metadata service][metadata].
///
/// The [authenticate] method is used by the [FirestoreGateway] to make
/// authenticated requests.
///
/// An optional [useEmulator] parameter allows authenticating with a local
/// Firebase emulator.
class ApplicationDefaultAuthenticator {
ApplicationDefaultAuthenticator({required this.useEmulator});

Expand Down
Loading