Adding login to your SwiftUI app with SuperTokens and NodeJS — Part 1

Adding login to your SwiftUI app with SuperTokens and NodeJS — Part 1

·

9 min read

Most modern-day mobile applications involve authentication in some way, this usually involves asking your users to log in 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

Let's start with a bare-bones SwiftUI app, we need two screens: The login screen and a home screen.

Let's 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. Let's 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. Let's 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, let's 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 a 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.