Tutorial
This section will require knowledge of writing server side HTTP endpoints and handlers. The server side sample code is written in JavaScript (NodeJS with an Express-like API), however you can use any framework or language of your choosing.
We will use Auth0 in this tutorial. But you can use any third-party identity provider. Each app can use multiple identity providers. Identity providers can be:
In this tutorial, you'll build a simple application so users can log in with a a third-party provider using Auth0. We assume that you have already completed the Auth0 tutorial on their documentation before starting this tutorial.
For the full application code in JavaScript and Swift, see the code samples on GitHub.
Server
The authentication webhook needs to handle an HTTP POST request. Each client that will need to authenticate will send a payload to this webhook. The following section requires that you have knowledge of writing server side HTTP endpoints and responding with a JSON payload. Code samples of server side code are written with a NodeJS / Express syntax. You can use any language or framework on the server side.
Incoming POST body
When your client device wants to authenticate using your webhook, your server will receive an HTTP post with a JSON payload that looks like:
{ "appID": "YOUR_APP_ID_HERE", // the appID "provider": "my-auth", // this is the "Name" of the "Authentication Webhook" "token": "eyJhbGciOiJI..." // this is what each device will send to authenticate}
Your can introspect these values by parsing out the request body:
let express = require('express')let cors = require('cors')let body = require('body-parser')let app = express()
app.use(cors())app.use(body.json())
let app = express()
app.post('/', (req, res) => { const appID = req.body.appID const provider = req.body.provider const token = req.body.token})
Generally, you will want to check the token for some sort of validity. Let's assume you have some sort of library or logic to parse and validate the token is for a specific user.
You can also use the userInfo
key in your JSON response to
pass information back to client.
app.post('/', async (req, res) => { const token = req.body.token; try { const { userId } = await checkToken(token) // payload = getDittoPermissions(userId) res.json(payload) } catch (err) { res.json({ "authenticate": err, "userInfo": err.message }) }})
As a simple example, let's grant full read
& write
permissions to all
collections and all documents.
app.post('/', async (req, res) => { const token = req.body.token; try { let payload = { "authenticate": true, "expirationSeconds": 28800, "userID": "123abc", "permissions": { "read": { "everything": true, "queriesByCollection": {} }, "write": { "everything": true, "queriesByCollection": {} } } } res.json(payload) } catch (err) { res.json({ "authenticate": err, "userInfo": err.message }) }})
For more information on how to design your app's permissions, see Access Control Permissions.
Deploy your server
Now, the portal will attempt to reach this server. That means you must deploy it somewhere that this HTTP request is accessible. For testing, you can use a quick-deploy service such as Glitch.
info
Please be sure that this endpoint is not behind a firewall or VPN. If you cannot get around this requirement contact us.
app.listen(process.env.PORT, () => { console.log('server listening on ', process.env.PORT)})
Configure your Portal App
To use an "Online With Authentication" identity, go to your app in the portal and find the Authentication Mode & Webhook Settings section. Ensure that "With Authentication" is selected like so:
Below, a section called Authentication Webhooks will be editable. Once your Authentication Webhook Endpoint(s) is deployed and ready, you can register it in the portal. Add a Name and URL.
- The Name is used the differentiate between multiple authentication. Most applications will have one authentication webhook, however the Name parameter is still required. This name value is important for the next section.
- The URL parameter is the fully qualified URL of the webhook that you deployed in the section above. Please include the protocol (https:// or http:// though we highly discourage http://).
Client
To configure your client application, you must first add URL of the POST endpoint you created in the previous section.
info
The provider name given to the Ditto Client must match a provider name in the Portal (e.g., my-auth
).
This tutorial assumes you've already configured your auth0
client from the
official Auth0 documentation.
Login
Assuming you have a login button in the HTML:
<button onClick={login}>Login</button>
We attach a login
function to the button.
import createAuth0Client from '@auth0/auth0-spa-js';// OR for Reactimport { useAuth0 } from '@auth0/auth0-react';
// configure your auth0 client...
async function login () { await auth0.loginWithRedirect({ redirect_uri: window.location.origin }); startDitto()}
We can then create a startDitto
function that gets the access token and starts a
new Ditto instance, and passes the token to your server route you created in the previous section.
import createAuth0Client from '@auth0/auth0-spa-js';// OR for Reactimport { useAuth0 } from '@auth0/auth0-react';import { init, Ditto } from "@dittolive/ditto"
// configure your auth0 client...
let ditto
(async () => { await init() // you need to call this at least once before using any of the Ditto API
function startDitto () { let token = await auth0.getAccessTokenSilently();
const authHandler = { authenticationRequired: async function(authenticator) { await authenticator.loginWithToken(token, "my-auth"); console.log("Login request completed."); }, authenticationExpiringSoon: function(authenticator, secondsRemaining) { console.log(`Auth token expiring in ${secondsRemaining} seconds`) await authenticator.loginWithToken(token, "my-auth"); console.log("Login request completed."); } }
const identity = { type: 'onlineWithAuthentication', appID: 'REPLACE_ME_WITH_YOUR_APP_ID', authHandler }
ditto = new Ditto(identity, '/persistence/file/path') ditto.startSync() }
async function login () { await auth0.loginWithRedirect({ redirect_uri: window.location.origin }); startDitto() }})()
To demonstrate that this Ditto client has been authenticated, let's display the number of cars in the collection, and a button to add one item to it:
<div> <h1>Cars: {numberOfCars}</h1> <button onClick={addItem}>+1</button></div>
Once we start the ditto instance, we can create a liveQuery
and create a
button that adds items to a collection:
let subscription = ditto.store.collection('cars').find("state == 'FOR_SALE'").subscribe()let liveQuery = ditto.store.collection('cars').find("name == 'Toyota'").observeLocal((cars) => { numberOfCars = cars.length})
function addItem () { ditto.store.collection('cars').upsert({ "name": 'Toyota', "state": 'FOR_SALE' })}
Assuming you have a login button in your SwiftUI ContentView, we want to create
a new ObservedObject
that we can subscribe to for updates to the
authentication status.
class ProfileViewModel: ObservableObject { // your authentication code will go here}
struct ContentView: View { @ObservedObject var viewModel: ProfileViewModel = ProfileViewModel()
var body: some View { Button("Login").padding() }}
We attach a login function to the button
class ProfileViewModel: ObservableObject { let credentialsManager = CredentialsManager(authentication: Auth0.authentication())
func login () { Auth0 .webAuth() .scope("openid profile") .audience("https://ENTER_YOUR_SCOPE_URL_HERE.auth0.com/userinfo") .start { result in switch result { case .success(let credentials): print("Obtained credentials: \(credentials)") self.credentialsManager.store(credentials: credentials) self.startDitto() case .failure(let error): print("Failed with: \(error)") // Handle Error } } }}
struct ContentView: View { @ObservedObject var viewModel: ProfileViewModel = ProfileViewModel()
var body: some View { Button("Login", action: viewModel.login).padding() }}
We can then create a startDitto function that:
- Gets the access token from Auth0;
- Starts the Ditto instance; and
- Creates a liveQuery
class ProfileViewModel: ObservableObject { @Published var ditto: Ditto? @Published var docs: [DittoDocument] = [] ....
func startDitto () { // 1. Get the access token from Auth0 credentialsManager.credentials { error, credentials in guard error == nil, let credentials = credentials else { // Handle error return } guard let accessToken = credentials.accessToken else { // Handle Error return } self.authDelegate = AuthDelegate(token: accessToken) // 2. Start the Ditto instance let identity = DittoIdentity.onlineWithAuthentication( appID: "YOUR_APP_ID_HERE", authenticationDelegate: self.authDelegate )
let ditto = Ditto(identity: identity) try! ditto.startSync()
// 3. Create a liveQuery self.ditto = ditto let subscription = ditto.store.collection("cars").find("state == 'FOR_SALE'").subscribe() let liveQuery = ditto.store.collection("cars").find("name == 'Toyota'").observeLocal { docs, event in self.docs = docs } } }}
To pass the token to your server route you created in the previous section, you
need to create an AuthDelegate
class that is passed to the Ditto constructor:
class AuthDelegate: DittoAuthenticationDelegate { var token: String init (token: String) { self.token = token }
func authenticationRequired(authenticator: DittoAuthenticator) { authenticator.loginWithToken(self.token, provider: "my-auth") { err in print("Login request completed. Error? \(err)") } }
func authenticationExpiringSoon(authenticator: DittoAuthenticator, secondsRemaining: Int64) { print("Auth token expiring in \(secondsRemaining)") authenticator.loginWithToken(self.token, provider: "my-auth") { err in print("Login request completed. Error? \(err)") } }}
Our ContentView can now display the number of cars, and you can add a button for adding an item to the database:
struct ContentView: View { @ObservedObject var viewModel: ProfileViewModel = ProfileViewModel() var body: some View { Button("Login", action: viewModel.login) .padding() } Text("Cars:" + String(viewModel.docs.count)) // Bonus points: implement addItem button using Ditto's `upsert` Button("+1", viewModel.addItem) }}
To make this usable for real-world applications, you can retreieve the user's profile details such as email, username, and full name. See the official Auth0 documentation for your platform to add that functionality to your application
Logout
First, we need some sort of way to monitor the state of the login flow. This allows us to display a Logout button when the user has already logged in.
let loggedIn = falseif (auth0.isAuthenticated()) { loggedIn = true}
if (loggedIn) { // render the logout button <button onClick={onLogoutClick}>Logout</button>} else { <button onClick={login}>Login</button>}
And then we can write the logout function and attach it to the button.
We also recommend calling ditto.auth.logout
with a callback function that
evicts any data from the local database.
function onLogoutClick() { ditto.auth.logout(() => { ditto.store.collection('cars').findAll().evict() }) await auth0.logout({ returnTo: window.location.origin })}
class ProfileViewModel: ObservableObject { @Published private(set) var state = State.isLoading
enum State { case isLoading case loaded(UserInfo) }
}
struct ContentView: View { @ObservedObject var viewModel: ProfileViewModel = ProfileViewModel() var body: some View { switch viewModel.state { case .isLoading: Button("Login", action: viewModel.login) case .loaded(let user): Text(user.name ?? "Anonymous Ditto User") Button("Logout", action: viewModel.logout) } Text("Cars:" + String(viewModel.docs.count)) }}
And then we can write the logout function and attach it to the button.
We also recommend calling ditto.auth.logout
with a callback function that
evicts any data from the local database.
class ProfileViewModel: ObservableObject { ... func logout () { Auth0 .webAuth() .clearSession(federated: false) { result in if result { if (self.ditto != nil) { // Clean up the cars collection after logout self.ditto!.auth?.logout(cleanup: { ditto in ditto.store.collection("cars").findAll().evict() }) } self.state = State.isLoading } } } }
🎉 You now have a fully functioning onlineWithAuthentication
app. Build and
run it on a device.
For the full application code in React and iOS, see the code samples on GitHub.