Adding login to your SwiftUI app with SuperTokens and NodeJS — Part 1
Tags: ios, authentication, swift, swiftui
Published: 2022-12-20 08:31:19

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](https://supertokens.com). 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:

```swift
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:


![Login Screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/at1xmhu5ccj4y2lzylyf.png)

## 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](https://supertokens.com/docs/emailpassword/introduction) to see additional options that you can configure and the different features you can use:

```bash
npx create-supertokens-app@latest
```

![Create SuperTokens App](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/btfyr3lbd4rzjwzorqke.png)

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:

```ts
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](https://supertokens.com/docs/emailpassword/architecture) 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](https://supertokens.com/docs/emailpassword/custom-ui/init/core/saas-setup) or [self hosting](https://supertokens.com/docs/emailpassword/custom-ui/init/core/with-docker) the core yourself.

```ts
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.

```ts
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.

```ts
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](https://supertokens.com/docs/emailpassword/pre-built-ui/securing-routes) 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:

```swift
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](https://supertokens.com/docs/emailpassword/common-customizations/changing-base-path/api-base-path).

```swift
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:

```swift
Button(action: {
    if self.isSigningUp {
        signUp()
    } else {
        signIn()
    }
}, label: {
    Text(self.isSigningUp ? "Sign Up" : "Sign In")
        .padding(.vertical, 12)
        .padding(.horizontal, 10)
})
```

```swift
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.

```swift
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](https://supertokens.com/docs/fdi).

## Building the home screen

```swift
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:

```swift
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](https://supertokens.com/).