Almost every application needs an authentication strategy. The most common being the classic username and password combo. However, there's a new approach some apps are taking to avoid handling or storing user passwords: passwordless authentication. It generally involves sending a one-time PIN (OTP) through a user-owned channel such as their phone or email. If the user inputs the correct PIN, we give them access to the system.
In this article, we'll build an iOS chat application with Stream Chat, for its fully featured iOS chat components and Auth0's passwordless features for authentication.
You can see the final result in the screenshot below and download the complete source code in the Passwordless Chat App iOS GitHub repository. Let's dive in!
Requirements
To follow this tutorial, create an account with Stream, Auth0, and Ngrok. You'll also need Xcode, NodeJS, and Ngrok installed.
Stream takes care of the chat features, Auth0 provides the passwordless authentication we need, and Ngrok lets us quickly expose our local NodeJS backend to the external world via HTTPS.
- A Stream account account
- An Auth0 account
- An Ngrok account
- Node v14 installed
- Ngrok installed
- Xcode 12 or later
Auth0's free plan includes the passwordless feature, and if you're an independent developer or with a small team, you may be eligible for Stream's free maker account. So, remember to sign up for it!
Create iOS Project
Since we'll need some details of the iOS project to configure the Auth0 dashboard, let's first create a UIKit project on Xcode with the UIKit App Delegate lifecycle.
Configure Auth0 Account
Before we can dive into the code, let's visit our Auth0 account to enable passwordless authentication and get the credentials we need.
After you've created your Auth0 account and you're in the dashboard, go to Authentication > Passwordless
and enable "Email". Also, make sure to set the Default App’s switch to “on” for passwordless authentication in the Applications tab. You can also use SMS if you have a Twilio account.
After you've enabled Email authentication, go to your Default App in Applications and copy your Domain and Client ID. We'll need these later in the iOS code section.
After that, scroll a bit down to Application Properties
and change your application type to Native
.
Once that's done, scroll further down to Advanced Settings > Device Settings
and paste your Team ID (DEVELOPMENT_TEAM) and App ID (PRODUCT_BUNDLE_IDENTIFIER), which you can find in your Xcode .pbxproj file, which is contained within the .xcodeproj file. To reveal the .pbxproj file, right click the .xcodeproj file and select Show Package Contents.
Next, move to the Grant Types tab, enable Passwordless OTP, and hit save.
After that, move to the Certificates tab, download your PEM certificate, name it public.pem
, and place it in the same folder as your backend's index.js
. You will use this certificate to verify the authentication request in your backend.
Create Backend Project
The backend consists of a single endpoint that generates a Stream JWT for a given user id. When the iOS app makes this request, it must also include an Auth0 ID Token to be verified. If the verification fails, we return 401 (Unauthorized) instead of the token. If it succeeds, we produce the Stream token, thus providing our user access to the chat feature under the given user id.
const express = require('express') const jwt = require('jsonwebtoken') const StreamChat = require('stream-chat').StreamChat const fs = require('fs') const app = express() const port = 3000 const auth0Public = fs.readFileSync('public.pem'); // get public key const client = new StreamChat('[stream_api_key]', '[stream_api_secret]') app.get('/', (req, res) => { const userId = req.query.userId const token = client.createToken(userId) const auth0Token = req.query.auth0Token console.log(auth0Token) try { var decoded = jwt.verify(auth0Token, auth0Public) res.send(token) } catch(err) { res.status(401).send('Unauthorized') console.log(err) } }) app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`) })
Install the dependencies with npm install express stream-chat jsonwebtoken --save
or yarn add express stream-chat jsonwebtoken
, then run your backend with node index.js
.
Using Ngrok
To access this endpoint outside our local machine and serve it under HTTPS (ideal for iOS apps), we can use Ngrok. You can install Ngrok through homebrew by running the command brew install ngrok
, or visit the Ngrok website. You'll also need an account and the auth token set up.
After you have Ngrok and your account set up, run ngrok http 3000
. If your backend is running on a different port than 3000, make sure to use that number instead. After you run the command, you should copy the https URL. You'll need it in the next section.
iOS Dependencies
Add the following dependencies to your iOS project.
You can install the dependencies with Xcode's built-in Swift Package Manager integration or CocoaPods.
iOS Code
First, we need code to ask our backend for the JWT. You can write a stand-alone function that does it like below. Remember to replace the Ngrok URL. You can paste the function anywhere you want. For example, you can create a fetchStreamJWT.swift file and paste it there.
import Foundation func fetchStreamJWT(_ auth0Token: String, completion: @escaping (String?) -> Void) { let url = URL(string: "https://89eedc9689f7.ngrok.io/?auth0Token=\(auth0Token)")! URLSession.shared.dataTask(with: url) { data, response, error in if let data = data { completion(String(data: data, encoding: .utf8)) } else { completion(nil) } }.resume() }
After that, we also need a function that creates our Chat's UI stack. After reading Stream Chat's iOS Tutorial, we can quickly write a stand-alone function that sets up our UI. As with the last function, you can create a makeChat.swift file and paste it there.
import UIKit import StreamChat import StreamChatUI func makeChat(_ token: String) -> UINavigationController { let apiKey = APIKey("p3peksa7mzmx") let token = Token(stringLiteral: token) let chatConfig = ChatClientConfig(apiKey: apiKey) let chatClient = ChatClient(config: chatConfig, tokenProvider: .static(token)) let channelListVC = ChatChannelListVC() let query = ChannelListQuery(filter: .containMembers(userIds: [chatClient.currentUserId!])) channelListVC.controller = chatClient.channelListController(query: query) return UINavigationController(rootViewController: channelListVC) }
In the ViewController.swift file, we'll use Auth0's Lock.swift SDK to show the authentication UI. Once the user inputs the OTP, the onAuth
callback is triggered, giving us the Auth0 ID token. We then call fetchStreamJWT
to hit our endpoint and makeChat
to display our chat UI.
import UIKit import Auth0 import Lock class ViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) Lock.passwordless(clientId: "[YOUR AUTH0 CLIENT ID]", domain: "[YOUR AUTH0 DOMAIN]") .withOptions { $0.oidcConformant = true } .withConnections { $0.email(name: "email") } .withStyle { $0.title = "Welcome to my App!" } .onAuth(callback: onAuth) .onError { print("Failed with \($0)") } .onCancel { print("User cancelled") } .onPasswordless { print("Passwordless requested for \($0)") } .present(from: self) } func onAuth(_ credentials: Credentials) { let window = view.window! if let idToken = credentials.idToken { fetchStreamJWT(idToken) { token in if let token = token { DispatchQueue.main.async { let nav = makeChat(token) UIView.transition( with: window, duration: 0.5, options: .transitionFlipFromLeft, animations: { window.rootViewController = nav }) } } } } else { print("no token") } } }
Testing Your App
If everything is right and your backend is running, you should be able to run your project in a device or simulator. Type in your email, and you'll receive a code. After typing the code correctly and pressing submit, it will take you to the chat screen.
Next Steps with the Passwordless Chat App
Congratulations! You've built the basis of a functioning passwordless chat app with Stream Chat and Auth0. I encourage you to browse through Stream Chat's docs, Auth0's iOS passwordless docs, and experiment with the project you just built. Good luck on your app development!