docs: add generate user tokens with firebase guide
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,448 @@
|
||||
---
|
||||
id: token_generation_with_firebase
|
||||
sidebar_position: 5
|
||||
title: User Token Generation With Firebase Auth and Cloud Functions
|
||||
---
|
||||
|
||||
Securely generate Stream Chat user tokens using Firebase Authentication and Cloud Functions.
|
||||
|
||||
:::note
|
||||
This guide assumes that you are familiar with Firebase Authentication and Cloud Functions for Flutter and using the Flutter Stream Chat SDK.
|
||||
:::
|
||||
|
||||
### Introduction
|
||||
|
||||
In this guide, you'll explore how you can use Firebase Auth as an authentication provider and create Firebase Cloud functions to securely
|
||||
generate Stream Chat user tokens.
|
||||
|
||||
You will use Stream's [NodeJS client](https://getstream.io/chat/docs/node/?language=javascript) for Stream account creation and
|
||||
token generation, and [Flutter Cloud Functions for Firebase](https://firebase.flutter.dev/docs/functions/overview) to invoke the cloud functions
|
||||
from your Flutter app.
|
||||
|
||||
Stream supports several different [backend clients](https://getstream.io/chat/sdk/#backend-clients) to integrate with your server. This guide only shows an easy way to integrate Stream Chat authentication using Firebase and Flutter.
|
||||
|
||||
### Flutter Firebase
|
||||
|
||||
See the [Flutter Firebase getting started](https://firebase.flutter.dev/docs/overview) docs for setup and installation instructions.
|
||||
|
||||
You will also need to add the [Flutter Firebase Authentication](https://firebase.flutter.dev/docs/auth/overview), and [Flutter Firebase Cloud Functions](https://firebase.flutter.dev/docs/functions/overview) packages to your app. Depending on the platform that you target, there may be specific configurations that you need to do.
|
||||
|
||||
#### Starting Code
|
||||
|
||||
The following code shows a basic application with **FirebaseAuth** and **FirebaseFunctions**.
|
||||
|
||||
You will extend this later to execute cloud functions.
|
||||
|
||||
```dart
|
||||
import 'package:cloud_functions/cloud_functions.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp();
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Auth(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Auth extends StatefulWidget {
|
||||
Auth({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AuthState createState() => _AuthState();
|
||||
}
|
||||
|
||||
class _AuthState extends State<Auth> {
|
||||
late FirebaseAuth auth;
|
||||
late FirebaseFunctions functions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
auth = FirebaseAuth.instance;
|
||||
functions = FirebaseFunctions.instance;
|
||||
}
|
||||
|
||||
final email = 'test@getstream.io';
|
||||
final password = 'password';
|
||||
|
||||
Future<void> createAccount() async {
|
||||
// Create Firebase account
|
||||
await auth.createUserWithEmailAndPassword(email: email, password: password);
|
||||
print('Firebase account created');
|
||||
}
|
||||
|
||||
Future<void> signIn() async {
|
||||
// Sign in with Firebase
|
||||
await auth.signInWithEmailAndPassword(email: email, password: password);
|
||||
print('Firebase signed in');
|
||||
}
|
||||
|
||||
Future<void> signOut() async {
|
||||
// Revoke Stream chat token.
|
||||
final callable = functions.httpsCallable('revokeStreamUserToken');
|
||||
await callable();
|
||||
print('Stream user token revoked');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AuthenticationState(
|
||||
streamUser: auth.authStateChanges(),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: createAccount,
|
||||
child: Text('Create account'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: signIn,
|
||||
child: Text('Sign in'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: signOut,
|
||||
child: Text('Sign out'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationState extends StatelessWidget {
|
||||
const AuthenticationState({
|
||||
Key? key,
|
||||
required this.streamUser,
|
||||
}) : super(key: key);
|
||||
|
||||
final Stream<User?> streamUser;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<User?>(
|
||||
stream: streamUser,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return (snapshot.data != null)
|
||||
? Text('Authenticated')
|
||||
: Text('Not Authenticated');
|
||||
}
|
||||
return Text('Not Authenticated');
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Running the above will give this:
|
||||
|
||||

|
||||
|
||||
The `Auth` widget handles all of the authentication logic. It initializes a `FirebaseAuth.instance` and uses that
|
||||
in the `createAccount`, `signIn` and `signOut` methods. There is a button to envoke each of these methods.
|
||||
|
||||
The `FirebaseFunctions.instance` will be used later in this guide.
|
||||
|
||||
The `AuthenticationState` widget listens to `auth.authStateChanges()` to display a message
|
||||
indicating if a user is authenticated.
|
||||
|
||||
### Firebase Cloud Functions
|
||||
|
||||
Firebase Cloud Functions allows you to extend Firebase with custom operations that an event can trigger:
|
||||
- **Internal event**: For example, when creating a new Firebase account this is automatically triggered.
|
||||
- **External event**: For example, directly calling a cloud function from your Flutter application.
|
||||
|
||||
To set up your local environment to deploy cloud functions, please see the
|
||||
[Cloud Functions getting started](https://firebase.flutter.dev/docs/overview) docs.
|
||||
|
||||
After initializing your project with cloud functions, you should have a **functions** folder in your project, including a `package.json` file.
|
||||
|
||||
There should be two dependencies already added, **firebase-admin** and **firebase-functions**. You will also need to add the **stream-chat** dependency.
|
||||
|
||||
Navigate to the **functions** folder and run `npm install stream-chat --save-prod`.
|
||||
|
||||
This will install the node module and add it as a dependency to `package.json`.
|
||||
|
||||
Now open `index.js` and add the following (this is the complete example):
|
||||
|
||||
```js
|
||||
const StreamChat = require('stream-chat').StreamChat;
|
||||
const functions = require("firebase-functions");
|
||||
const admin = require("firebase-admin");
|
||||
|
||||
admin.initializeApp();
|
||||
|
||||
const serverClient = StreamChat.getInstance(functions.config().stream.key, functions.config().stream.secret);
|
||||
|
||||
|
||||
// When a user is deleted from Firebase their associated Stream account is also deleted.
|
||||
exports.deleteStreamUser = functions.auth.user().onDelete((user, context) => {
|
||||
return serverClient.deleteUser(user.uid);
|
||||
});
|
||||
|
||||
// Create a Stream user and return auth token.
|
||||
exports.createStreamUserAndGetToken = functions.https.onCall(async (data, context) => {
|
||||
// Checking that the user is authenticated.
|
||||
if (!context.auth) {
|
||||
// Throwing an HttpsError so that the client gets the error details.
|
||||
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
|
||||
'while authenticated.');
|
||||
} else {
|
||||
try {
|
||||
// Create user using the serverClient.
|
||||
await serverClient.upsertUser({
|
||||
id: context.auth.uid,
|
||||
name: context.auth.token.name,
|
||||
email: context.auth.token.email,
|
||||
image: context.auth.token.image,
|
||||
});
|
||||
|
||||
/// Create and return user auth token.
|
||||
return serverClient.createToken(context.auth.uid);
|
||||
} catch (err) {
|
||||
console.error(`Unable to create user with ID ${context.auth.uid} on Stream. Error ${err}`);
|
||||
// Throwing an HttpsError so that the client gets the error details.
|
||||
throw new functions.https.HttpsError('aborted', "Could not create Stream user");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get Stream user token.
|
||||
exports.getStreamUserToken = functions.https.onCall((data, context) => {
|
||||
// Checking that the user is authenticated.
|
||||
if (!context.auth) {
|
||||
// Throwing an HttpsError so that the client gets the error details.
|
||||
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
|
||||
'while authenticated.');
|
||||
} else {
|
||||
try {
|
||||
return serverClient.createToken(context.auth.uid);
|
||||
} catch (err) {
|
||||
console.error(`Unable to get user token with ID ${context.auth.uid} on Stream. Error ${err}`);
|
||||
// Throwing an HttpsError so that the client gets the error details.
|
||||
throw new functions.https.HttpsError('aborted', "Could not get Stream user");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Revoke the authenticated user's Stream chat token.
|
||||
exports.revokeStreamUserToken = functions.https.onCall((data, context) => {
|
||||
// Checking that the user is authenticated.
|
||||
if (!context.auth) {
|
||||
// Throwing an HttpsError so that the client gets the error details.
|
||||
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
|
||||
'while authenticated.');
|
||||
} else {
|
||||
try {
|
||||
return serverClient.revokeUserToken(context.auth.uid);
|
||||
} catch (err) {
|
||||
console.error(`Unable to revoke user token with ID ${context.auth.uid} on Stream. Error ${err}`);
|
||||
// Throwing an HttpsError so that the client gets the error details.
|
||||
throw new functions.https.HttpsError('aborted', "Could not get Stream user");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
First, you import the necessary packages and call `admin.initializeApp();` to set up Firebase cloud functions.
|
||||
|
||||
Next, you initialize the **StreamChat** server client by calling `StreamChat.getInstance`. This function requires your Stream app's
|
||||
**token** and **secret**. You can get this from the Stream Dashboard for your app.
|
||||
|
||||
Set these values as environment data on Firebase Functions.
|
||||
|
||||
```bash
|
||||
firebase functions:config:set stream.key="app-key" stream.secret="app-secret"
|
||||
```
|
||||
|
||||
*Replace **app-key** and **app-secret** with the values for your Stream app.*
|
||||
|
||||
This creates an object of **stream** with properties **key** and **secret**. To access this environment
|
||||
data use `functions.config().stream.key` and `functions.config().stream.secret`.
|
||||
|
||||
See the [Firebase environment configuration](https://firebase.google.com/docs/functions/config-env)
|
||||
documentation for additional information.
|
||||
|
||||
To deploy these functions to Firebase, run:
|
||||
|
||||
```bash
|
||||
firebase deploy --only functions
|
||||
```
|
||||
|
||||
### Create a Stream User and Get the User's Token
|
||||
|
||||
In the `createStreamUserAndGetToken` cloud function you create an `onCall` HTTPS handler, which exposes
|
||||
a cloud function that can be envoked from your Flutter app.
|
||||
|
||||
```js
|
||||
// Create a Stream user and return auth token.
|
||||
exports.createStreamUserAndGetToken = functions.https.onCall(async (data, context) => {
|
||||
// Checking that the user is authenticated.
|
||||
if (!context.auth) {
|
||||
// Throwing an HttpsError so that the client gets the error details.
|
||||
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
|
||||
'while authenticated.');
|
||||
} else {
|
||||
try {
|
||||
// Create user using the serverClient.
|
||||
await serverClient.upsertUser({
|
||||
id: context.auth.uid,
|
||||
name: context.auth.token.name,
|
||||
email: context.auth.token.email,
|
||||
image: context.auth.token.image,
|
||||
});
|
||||
|
||||
/// Create and return user auth token.
|
||||
return serverClient.createToken(context.auth.uid);
|
||||
} catch (err) {
|
||||
console.error(`Unable to create user with ID ${context.auth.uid} on Stream. Error ${err}`);
|
||||
// Throwing an HttpsError so that the client gets the error details.
|
||||
throw new functions.https.HttpsError('aborted', "Could not create Stream user");
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This function first does a check to see that the client that calls it is authenticated,
|
||||
by ensuring that `context.auth` is not null. If it is null, then it throws an `HttpsError` with a descriptive
|
||||
message. This error can be caught in your Flutter application.
|
||||
|
||||
If the caller is authenticated the function proceeds to use the `serverClient` to create a new Stream Chat
|
||||
user by calling the `upsertUser` method and passing in some user data. It uses the authenticated caller's **uid** as an **id**.
|
||||
|
||||
After the user is created it generates a token for that user. This token is then returned to the caller.
|
||||
|
||||
To call this from Flutter, you will need to use the `cloud_functions` package.
|
||||
|
||||
Update the **createAccount** method in your Flutter code to the following:
|
||||
|
||||
```dart
|
||||
Future<void> createAccount() async {
|
||||
// Create Firebase account
|
||||
await auth.createUserWithEmailAndPassword(email: email, password: password);
|
||||
print('Firebase account created');
|
||||
|
||||
// Create Stream user and get token
|
||||
final callable = functions.httpsCallable('createStreamUserAndGetToken');
|
||||
final results = await callable();
|
||||
print('Stream account created, token: ${results.data}');
|
||||
}
|
||||
```
|
||||
|
||||
Calling this method will do the following:
|
||||
1. Create a new Firebase User and authenticate that user.
|
||||
2. Call the `createStreamUserAndGetToken` cloud function and get the Stream user token for the authenticated user.
|
||||
|
||||
As you can see, calling a cloud function is easy and will also send all the necessary user authentication information (such as the UID)
|
||||
in the request.
|
||||
|
||||
Once you have the Stream user token, you can authenticate your Stream Chat user as you normally would.
|
||||
|
||||
Please see our [initialization documention](https://getstream.io/chat/docs/flutter-dart/init_and_users/?language=dart) for more information.
|
||||
|
||||
As you can see below, the User ID matches on both Firebase's and Stream's user database.
|
||||
|
||||
##### Firebase Authentication Database
|
||||
|
||||

|
||||
|
||||
##### Stream Chat User Database
|
||||
|
||||

|
||||
|
||||
|
||||
### Get the Stream User Token
|
||||
|
||||
The `getStreamUserToken` cloud function is very similar to the `createStreamUserAndGetToken` function. The only difference is
|
||||
that it only creates a user token and does not create a new user account on Stream.
|
||||
|
||||
Update the **signIn** method in your Flutter code to the following:
|
||||
|
||||
```dart
|
||||
Future<void> signIn() async {
|
||||
// Sign in with Firebase
|
||||
await auth.signInWithEmailAndPassword(email: email, password: password);
|
||||
print('Firebase signed in');
|
||||
|
||||
// Get Stream user token
|
||||
final callable = functions.httpsCallable('getStreamUserToken');
|
||||
final results = await callable();
|
||||
print('Stream user token retrieved: ${results.data}');
|
||||
}
|
||||
```
|
||||
|
||||
Calling this method will do the following:
|
||||
1. Sign in using Firebase Auth.
|
||||
2. Call the `getStreamUserToken` cloud function to get a Stream user token.
|
||||
|
||||
:::note
|
||||
The user needs to be authenticated to call this cloud function. Otherwise, the function will throw
|
||||
the **failed-precondition** error that you specified.
|
||||
:::
|
||||
|
||||
### Revoke Stream User Token
|
||||
|
||||
You may also want to revoke the Stream user token if you sign out from Firebase.
|
||||
|
||||
Update the `signOut` method in your Flutter code to the following:
|
||||
|
||||
```dart
|
||||
Future<void> signOut() async {
|
||||
// Revoke Stream user token.
|
||||
final callable = functions.httpsCallable('revokeStreamUserToken');
|
||||
await callable();
|
||||
print('Stream user token revoked');
|
||||
|
||||
// Sign out Firebase.
|
||||
await auth.signOut();
|
||||
print('Firebase signed out');
|
||||
}
|
||||
```
|
||||
:::note
|
||||
Call the cloud function before signing out from Firebase.
|
||||
:::
|
||||
|
||||
### Delete Stream User
|
||||
|
||||
When deleting a Firebase user account, it would make sense also to delete the
|
||||
associated Stream user account.
|
||||
|
||||
The cloud function looks like this:
|
||||
|
||||
```js
|
||||
// When a user is deleted from Firebase their associated Stream account is also deleted.
|
||||
exports.deleteStreamUser = functions.auth.user().onDelete((user, context) => {
|
||||
return serverClient.deleteUser(user.uid);
|
||||
});
|
||||
```
|
||||
|
||||
In this function, you are listening to delete events on Firebase auth. When an account is deleted, this function will be triggered, and you can get the
|
||||
user's **uid** and call the `deleteUser` method on the `serverClient`.
|
||||
|
||||
This is not an external cloud function; it can only be triggered when an
|
||||
account is deleted.
|
||||
|
||||
### Conclussion
|
||||
|
||||
In this guide, you have seen how to securely create Stream Chat tokens using
|
||||
Firebase Authentication and Cloud Functions.
|
||||
|
||||
The principles shown in this guide can be applied to your preferred authentication
|
||||
provider and cloud architecture of choice.
|
||||
Reference in New Issue
Block a user