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.
- 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.
- 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) } } } }
- 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
- First, access the parra Environment Value.
@Environment(\.parra) private var parra
- In your View, invoke the
updatePersonalInfo
function on Parra'suser
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.