Authentication

Parra offers the ability to include authentication in your app with minimal configuration. Just configure the authentication methods you want to support in the dashboard, an implement one of the provided Parra auth window views. We currently have support for the following authentication methods, and will be adding more soon.

  • Email/password
  • Passwordless via SMS OTP code
  • Passkeys

Once your users are authenticated, their personal information and access token are securely stored on their device. The contents of these objects is encrypted and the key is stored in the system Keychain.

Choosing an Authentication Flow

Generally, there are two styles of authentication used in mobile applications. Some require users to be logged in before they can use any of the app's functionality. Others allow users to use parts of the app without logging in. Parra provides an auth window view to use in either of these cases. Both wrap your app's main content view and handle conditional rendering of different UI elements or values of Environment Values depending on auth state.

The ParraOptionalAuthWindow should be used if you want your users to use parts of your app without logging in. This includes apps that don't require authentication.

ParraApp(
    tenantId: "my-tenant-id",
    applicationId: "my-application-id",
    appDelegate: appDelegate
) {
    WindowGroup {
        ParraOptionalAuthWindow {
            ContentView()
        }
    }
}

If you'd like to offer users the option to sign in, or want to gate a certain feature behind a login, you can use the presentParraSignInView modifier to present a sign in sheet modally. When the user finishes interacting with the sheet, a callback will inform you whether they signed in as a result. You can also monitor for changes to the parraAuthState Environment Value.

import Parra
import SwiftUI

struct ProfileView: View {
    @State private var isSigningIn = false

    var body: some View {
        Button("Sign in") {
            isSigningIn = true
        }
        .presentParraSignInView(isPresented: $isSigningIn) { dismissType in
            switch dismissType {
            case .cancelled:
                ParraLogger.info("User cancelled sign in")
            case .completed:
                ParraLogger.info("User successfully signed in")
            case .failed(let error):
                ParraLogger.error("User sign in failed: \(error)")
            }
        }
    }
}

The ParraRequiredAuthWindow handles the other case. When users aren't logged in, a login screen will be presented. When they complete login with one of the auth methods configured in the dashboard, the login screen will be dismissed, revealing your app's ContentView. Your ContentView isn't rendered until this point, so it's safe to rely on auth state of other Environment Values provided by Parra.

ParraApp(
    tenantId: "my-tenant-id",
    applicationId: "my-application-id",
    appDelegate: appDelegate
) {
    WindowGroup {
        ParraRequiredAuthWindow {
            ContentView()
        }
    }
}

If you're using the ParraRequiredAuthWindow and want to provide a custom experience for your login screen, this is achievable by providing a value for the unauthenticatedContent parameter of its initializer. If this is omitted, an instance of ParraDefaultAuthenticationFlowView is used (as shown below)

ParraApp(
    tenantId: "my-tenant-id",
    applicationId: "my-application-id",
    appDelegate: appDelegate
) {
    WindowGroup {
        ParraRequiredAuthWindow {
            ContentView()
        } unauthenticatedContent: {
            ParraDefaultAuthenticationFlowView(flowConfig: .default)
        }
    }
}

Accessing Identities

To access information about the current user, you'll need to use the parraAuthState Environment Value in a SwiftUI View.

@Environment(\.parraAuthState) private var parraAuthState

The resulting ParraAuthState enum has the following cases

  • authenticated: The user is logged in via an auth method like email/password, passkey, etc. Has a ParraUser associated value.
  • anonymous: The user is not logged in and anonymous authentication is enabled in the dashboard. Has a ParraUser associated value.
  • guest: The user is not logged in and anonymous authentication is disabled. Has a ParraGuest associated value.
  • error: An authentication error has occurred.
  • undetermined: An auth state has yet to be determined. This is the default state when the app launches until the previous auth state can be loaded from disk.

For convenience, you can access the optional computed user property, which will be set when in the authenticated or anonymous states. This user object contains an info object, with information about the user like their name and identities, as well as a credential object, which contains their current access token. The ParraGuest object contains this same credential object but does not include a user info object.

Authenticating with your Backend

Parra stores user authentication information in JSON web tokens (JWT), which means you can securely pass them to your own backend and validate the user they belong to. The example below demonstrates how to do this for a Node backend with JavaScript and the jwt-decode library. The process will be similar with other backend frameworks.

  1. Create an API key for your backend to decode the JWT. It is important that this key never be shared, store in your git repo or included in your app.
  2. Read the user's access token from the parraAuthState Environment Value and attach the token to an HTTP request made to your backend.
    import Parra
    import SwiftUI
    
    struct ContentView: View {
        @Environment(\.parraAuthState) private var parraAuthState
    
        var body: some View {
            Button("Make request", action: makeRequestToBackend)
        }
    
        private func makeRequestToBackend() {
            let accessToken = parraAuthState.user?.credential.accessToken
    
            Task {
                var request = URLRequest(
                    url: URL(string: "https://api.myapp.com/my-endpoint")!
                )
    
                request.setValue(accessToken, forHTTPHeaderField: "PARRA-ACCESS-TOKEN")
    
                do {
                    try await URLSession.shared.data(for: request)
                } catch {
                    ParraLogger.error("Error making request to backend", error)
                }
            }
        }
    }
    
  3. Retrieve the access token from the incoming request and use the API key you previously obtained to access its contents.
    const express = require('express');
    const jwt = require('jwt-decode');
    const router = express.Router();
    
    require('dotenv').config();
    
    router.get('/my-endpoint', (req, res) => {
        const token = req.headers['PARRA-ACCESS-TOKEN'];
    
        if (!token) {
            return res.status(401).json({ error: 'No token provided' });
        }
    
        try {
            const decoded = jwt.verify(token, process.env.PARRA_API_KEY_SECRET);
            res.json({ message: 'Access granted', user: decoded });
        } catch (error) {
            res.status(401).json({ error: 'Invalid token' });
        }
    });
    
    module.exports = router;
    

Profile Management

Parra provides APIs that you can use to allow your users to make modifications to their profiles. This includes updating their personal information and changing their profile photo.

Update Profile Info

  1. First, access the parra Environment Value.
    @Environment(\.parra) private var parra
    
  2. In your View, invoke the updatePersonalInfo function on Parra's user module to update the profile info for the current user.
    try await parra.user.updatePersonalInfo(
        name: "new display name",
        firstName: "new first name",
        lastName: "new last name"
    )
    

Update Profile Photo

Allowing your users to upload profile photos is as easy as dropping the ParraProfilePhotoWell() View into your view hierarchy. Logged in users will automatically be able to choose between taking new profile photos, uploading from their camera roll, or deleting their existing photo.

Was this page helpful?