Adding login to your SwiftUI app with SuperTokens and NodeJS — Part 1
Nemi Shah
Posted on December 20, 2022
Most modern day mobile applications involve authentication in some way, this usually involves asking your users to login to your app before they get to use it. In this article we will cover how to build a simple email password based login system for SwiftUI.
A basic login system would include the following:
- A form where the user can enter their information
- An API layer that will verify the credentials and log the user in
- A way for the frontend to verify that the user is logged in and can access the app
For the API layer and authentication we will use an Express server that integrates with SuperTokens. SuperTokens is an open source user authentication solution, it makes building authentication super simple and provides SDKs for various languages (we will use the NodeJS one for this article)
Building the app
Lets start with a bare bones SwiftUI app, we need two screens: The login screen and a home screen.
Lets start with the login screen:
struct LoginView: View {
@State private var enteredEmail = ""
@State private var enteredPassword = ""
@State private var isSigningUp = false
var body: some View {
VStack {
Text(self.isSigningUp ? "Sign Up" : "Sign In")
.font(.system(size: 26, weight: .medium))
TextField("Email", text: self.$enteredEmail)
.padding(.vertical, 12)
.padding(.horizontal, 12)
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(
Color.black.opacity(0.4),
style: StrokeStyle()
)
)
.foregroundColor(Color.black)
SecureField("Password", text: self.$enteredPassword)
.padding(.vertical, 12)
.padding(.horizontal, 12)
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(
Color.black.opacity(0.4),
style: StrokeStyle()
)
)
.foregroundColor(Color.black)
.padding(.top)
Button(action: {
self.isSigningUp = !self.isSigningUp
}, label: {
Text(self.isSigningUp ? "Already have an account?" : "Dont have an account?")
})
.padding(.top, 8)
Button(action: {
}, label: {
Text(self.isSigningUp ? "Sign Up" : "Sign In")
.padding(.vertical, 12)
.padding(.horizontal, 10)
})
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(Color.white)
.padding(.top)
}
.padding()
}
}
This will render the following:
Integrating with SuperTokens
The home screen of the app will need a way to check if a user is logged in and need access to some information about the user, so before we build the screen we will first build the authentication layer using SuperTokens.
SuperTokens provides a command line tool that lets us get started quickly, you can refer to the official documentation to see additional options that you can configure and the different features you can use:
npx create-supertokens-app@latest
For the frontend framework you can choose any option since we will be using our SwiftUI app anyway, for the backend framework choose Node.js to generate an express app that uses SuperTokens. Once the tool is done it will generate a frontend and backend app under a folder names my-app
, we will only use the backend
folder for the sake of this article. Lets take a look at the generated code:
import express from "express";
import cors from "cors";
import supertokens from "supertokens-node";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { middleware, errorHandler, SessionRequest } from "supertokens-node/framework/express";
import EmailPassword from "supertokens-node/recipe/emailpassword";
import Session from "supertokens-node/recipe/session";
supertokens.init({
supertokens: {
// this is the location of the SuperTokens core.
connectionURI: "https://try.supertokens.com",
},
appInfo: {
appName: "SuperTokens Demo App",
apiDomain: "http://localhost:3001",
websiteDomain: "http://localhost:3000",
},
// recipeList contains all the modules that you want to
// use from SuperTokens. See the full list here: https://supertokens.com/docs/guides
recipeList: [
EmailPassword.init(),
Session.init(),
],
});
const app = express();
app.use(
cors({
origin: "http://localhost:3000",
allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()],
methods: ["GET", "PUT", "POST", "DELETE"],
credentials: true,
})
);
// This exposes all the APIs from SuperTokens to the client.
app.use(middleware());
// An example API that requires session verification
app.get("/sessioninfo", verifySession(), async (req: SessionRequest, res) => {
let session = req.session;
res.send({
sessionHandle: session!.getHandle(),
userId: session!.getUserId(),
accessTokenPayload: session!.getAccessTokenPayload(),
});
});
// In case of session related errors, this error handler
// returns 401 to the client.
app.use(errorHandler());
app.listen(3001, () => console.log(`API Server listening on port 3001`));
The generated code is a simple express application that uses the supertokens-node
SDK to integrate email password login and session management. The SuperTokens SDK exposes a set of APIs that the frontend can call to implement the login functionality, to read more about how SuperTokens works you can refer to the how it works section of the documentation. Lets cover some of the major parts of the code:
supertokens.init
initialises SuperTokens, connectionURI
is used to connect to the core where all the logic for authentication resides. In the example above we use try.supertokens.com
which is a demo core hosted by the SuperTokens team, when you’re ready to release your app make sure to change this URL by following the guide for using the managed service or self hosting the core yourself.
recipeList: [
EmailPassword.init(),
Session.init(),
],
This part of the code tells SuperTokens which modules you want to enable, for this example we enable the email and password login mechanism but we could also enable features such as user roles, email verification etc. You can refer to the official documentation to know all the features that can be used.
app.use(middleware());
// ...
app.use(errorHandler());
The middleware
exposes all the APIs that SuperTokens can handle including the sign and sign up APIs that we will use in this example. The errorHandler
lets the SuperTokens SDK handle some of the failures such as returning 401
when the user is not logged in.
app.get("/sessioninfo", verifySession(), async (req: SessionRequest, res) => {
let session = req.session;
res.send({
sessionHandle: session!.getHandle(),
userId: session!.getUserId(),
accessTokenPayload: session!.getAccessTokenPayload(),
});
});
This API can be used to display some information to the user, the verifySession
middleware used in the example makes sure that the API logic is executed only if the user has a valid session (i.e they are logged in), and it will result in a 401
if they are not. Refer to the documentation to learn more about this. Note that this API is simply an example that is provided with the generated application.
Run npm run start
in the backend directory to start the API server, at this point we can leave the backend server running and get back to our frontend. Note that we will be using our machine’s IP instead of localhost
.
Adding SuperTokens to the iOS app
If you haven’t already, start with enabling Cocoapods for the app by running pod init
.
pod 'SuperTokensIOS'
Add this to your Podfile and run pod install
to install the iOS SDK for SuperTokens. We need to initialise SuperTokens, you want to make sure this is done at the starting point of your app:
import SwiftUI
import SuperTokensIOS
@main
struct LoginWithSuperTokensApp: App {
init() {
do {
try SuperTokens.initialize(apiDomain: "http://192.168.29.87:3001")
} catch {
// Error initialising SuperTokens
}
URLProtocol.registerClass(SuperTokensURLProtocol.self)
}
var body: some Scene {
WindowGroup {
LoginView()
}
}
}
The above code snippet sets up session management network interceptors on the frontend. Our frontend SDK will now be able to automatically save and add session tokens to each request to your API layer and also do auto session refreshing
This is from the official docs, in essence initialising the SDK lets SuperTokens handle sessions for you and handle automatically refreshing them if the server returns 401
. apiDomain
tells SuperTokens where your API layer is hosted, since we have our server running locally we use the local IP address. By default SuperTokens considers that the auth APIs are exposed via the /auth
route, if you want to change that refer to the documentation.
URLProtocol.registerClass(SuperTokensURLProtocol.self)
This adds the SuperTokens protocol to the list of interceptors for the default URLSession configuration so that all relevant requests are automatically handled by the SDK.
Implementing login
Let's revisit the login view and add the logic to sign in/sign up the user. First lets change the button to call either sign in or up depending on what the current UI state is:
Button(action: {
if self.isSigningUp {
signUp()
} else {
signIn()
}
}, label: {
Text(self.isSigningUp ? "Sign Up" : "Sign In")
.padding(.vertical, 12)
.padding(.horizontal, 10)
})
func signIn() {
if self.enteredEmail == "" || self.enteredPassword == "" {
return
}
var request = URLRequest(url: URL(string: "http://192.168.29.87:3001/auth/signin")!)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let formFields: [[String : Any]] = [
[
"id": "email",
"value": self.enteredEmail
],
[
"id": "password",
"value": self.enteredPassword
]
]
let body: [String: Any] = [
"formFields": formFields
]
let data = try! JSONSerialization.data(withJSONObject: body)
request.httpBody = data
URLSession.shared.dataTask(with: request, completionHandler: {
data, response, error in
if data != nil, let json: [String : Any] = try? JSONSerialization.jsonObject(with: data!) as? [String: Any] {
if json["status"] as! String == "OK" {
// Sign In succeeded
} else {
print(json["status"])
}
}
}).resume()
}
This will call the /auth/signin
API that SuperTokens exposes on the backend. If you sign in with your email you will get a WRONG_CREDENTIALS_ERROR
status in the response because a user with that email does not exist yet.
func signUp() {
var request = URLRequest(url: URL(string: "http://192.168.29.87:3001/auth/signup")!)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let formFields: [[String : Any]] = [
[
"id": "email",
"value": self.enteredEmail
],
[
"id": "password",
"value": self.enteredPassword
]
]
let body: [String: Any] = [
"formFields": formFields
]
let data = try! JSONSerialization.data(withJSONObject: body)
request.httpBody = data
URLSession.shared.dataTask(with: request, completionHandler: {
data, response, error in
if data != nil, let json: [String : Any] = try? JSONSerialization.jsonObject(with: data!) as? [String: Any] {
if json["status"] as! String == "OK" {
// Sign Up succeeded
} else {
print(json["status"])
}
}
}).resume()
}
This will call the /auth/signup
API that SuperTokens exposes on the backend. You can find a list of all APIs exposed by the SuperTokens SDK here.
Building the home screen
import SwiftUI
import SuperTokensIOS
struct HomeView: View {
var body: some View {
var doesSessionExist = SuperTokens.doesSessionExist()
var text = "Session exists: \(doesSessionExist)"
var userId = ""
if doesSessionExist {
do {
userId = try SuperTokens.getUserId()
} catch {
}
}
return VStack {
Text(text)
Text("User ID: \(userId)")
}
}
}
SuperTokens provides some helper functions such as doesSessionExist
, getUserId
and signOut
that can be used for some fundamental auth functionality. You can use doesSessionExist
to check if a user has a valid session to protect parts of your app.
Showing the home screen by default
One last thing to do is to change the main app struct to show the home screen by default for logged in users:
var body: some Scene {
WindowGroup {
if SuperTokens.doesSessionExist() {
HomeView()
} else {
LoginView()
}
}
}
And there you have it, you can very easily add login to your app using SuperTokens. In future articles we will cover how we can build a similar app but using passwordless and social login mechanisms instead of email password. If you’d like to learn more about the features SuperTokens provides and how they work visit their website.
Posted on December 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.