Auth0 authentication for Skip apps on both iOS and Android.
On iOS this wraps the Auth0.swift SDK (v2.18+). On Android, the Swift code is transpiled to Kotlin via Skip Lite and wraps the Auth0.Android SDK (v3.14).
Add the dependency to your Package.swift file:
let package = Package(
name: "my-package",
products: [
.library(name: "MyProduct", targets: ["MyTarget"]),
],
dependencies: [
.package(url: "https://source.skip.dev/skip-auth0.git", "0.0.0"..<"2.0.0"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "SkipAuth0", package: "skip-auth0")
])
]
)- Create an Auth0 account and tenant.
- Create a Native application in your Auth0 dashboard.
- Note your Domain and Client ID.
- Configure Allowed Callback URLs and Allowed Logout URLs for both platforms (see below).
- If you plan to use
loginWithCredentialsorsignUp, enable the Password grant type in your application's advanced settings under Grant Types.
Add your Auth0 domain and client ID to your Info.plist, and register the callback URL scheme:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>YOUR_BUNDLE_ID</string>
</array>
</dict>
</array>In your Auth0 dashboard, add the callback URL: YOUR_BUNDLE_ID://YOUR_AUTH0_DOMAIN/ios/YOUR_BUNDLE_ID/callback
Add your Auth0 domain and scheme to your AndroidManifest.xml via Gradle manifest placeholders. In the test target's Skip/skip.yml this is configured as:
build:
contents:
- block: 'android'
contents:
- block: 'defaultConfig'
contents:
- 'manifestPlaceholders["auth0Domain"] = "YOUR_AUTH0_DOMAIN"'
- 'manifestPlaceholders["auth0Scheme"] = "YOUR_SCHEME"'In your Auth0 dashboard, add the callback URL: YOUR_SCHEME://YOUR_AUTH0_DOMAIN/android/YOUR_PACKAGE_NAME/callback
See the Auth0 Android quickstart for details.
Configure the SDK early in your app's lifecycle:
import SkipAuth0
@main struct MyApp: App {
init() {
Auth0SDK.shared.configure(Auth0Config(
domain: "yourapp.us.auth0.com",
clientId: "your-client-id",
scheme: "com.example.myapp" // Your URL scheme
))
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}The recommended approach is Auth0's Universal Login, which opens the system browser:
Auth0SDK.shared.login { result in
switch result {
case .success(let credentials):
print("Logged in!")
print("Access token: \(credentials.accessToken ?? "")")
print("ID token: \(credentials.idToken ?? "")")
print("Refresh token: \(credentials.refreshToken ?? "")")
print("Expires at: \(credentials.expiresAt?.description ?? "")")
case .failure(let error):
print("Login failed: \(error)")
}
}With custom scopes and audience:
Auth0SDK.shared.login(
scope: "openid profile email offline_access",
audience: "https://api.example.com"
) { result in
// Handle result...
}For direct login without opening a browser (requires enabling the "Password" grant type in your Auth0 application):
Auth0SDK.shared.loginWithCredentials(
email: "user@example.com",
password: "securepassword"
) { result in
switch result {
case .success(let credentials):
print("Logged in with credentials")
case .failure(let error):
print("Login failed: \(error)")
}
}Create a new user account and immediately log in to obtain tokens:
Auth0SDK.shared.signUp(
email: "newuser@example.com",
password: "securepassword"
) { result in
switch result {
case .success(let credentials):
print("Signed up and logged in!")
print("Access token: \(credentials.accessToken ?? "")")
case .failure(let error):
print("Sign up failed: \(error)")
}
}With additional options:
Auth0SDK.shared.signUp(
email: "newuser@example.com",
password: "securepassword",
username: "newuser", // If your connection requires usernames
connection: "Username-Password-Authentication",
scope: "openid profile email offline_access",
audience: "https://api.example.com"
) { result in
// Handle result...
}Prerequisites: The "Password" grant type must be enabled in your Auth0 application settings. The database connection must allow sign-ups (check Disable Sign Ups is unchecked in your connection settings).
Create a new user without logging in. Useful when an admin creates accounts or when you want to handle login separately:
Auth0SDK.shared.createUser(
email: "newuser@example.com",
password: "securepassword"
) { result in
switch result {
case .success(let user):
print("User created: \(user.email)")
print("Username: \(user.username ?? "none")")
print("Email verified: \(user.emailVerified)")
case .failure(let error):
print("User creation failed: \(error)")
}
}Send a password reset email to the user:
Auth0SDK.shared.resetPassword(email: "user@example.com") { result in
switch result {
case .success:
print("Password reset email sent")
case .failure(let error):
print("Password reset failed: \(error)")
}
}Note: For security reasons, this method will not fail if the email address does not exist in the database. The user will simply not receive an email.
Fetch the user's profile from the Auth0 /userinfo endpoint:
Auth0SDK.shared.userInfo(accessToken: credentials.accessToken!) { result in
switch result {
case .success(let profile):
print("User ID: \(profile.sub)")
print("Name: \(profile.name ?? "")")
print("Email: \(profile.email ?? "")")
print("Email verified: \(profile.emailVerified)")
print("Picture: \(profile.picture ?? "")")
print("Nickname: \(profile.nickname ?? "")")
case .failure(let error):
print("Failed to fetch profile: \(error)")
}
}Prerequisites: Requires a valid access token from a login flow. The token must have the
openidscope to return user claims.
Revoke a refresh token to invalidate it server-side:
Auth0SDK.shared.revokeToken(refreshToken: savedRefreshToken) { result in
switch result {
case .success:
print("Token revoked")
case .failure(let error):
print("Revocation failed: \(error)")
}
}Note: After revocation, the refresh token can no longer be used to obtain new access tokens. Call this during logout for complete session invalidation.
Auth0SDK.shared.logout { result in
switch result {
case .success:
print("Logged out")
case .failure(let error):
print("Logout failed: \(error)")
}
}
// Federated logout (also logs out of the identity provider)
Auth0SDK.shared.logout(federated: true) { result in
// Handle result...
}Refresh an expired access token using a refresh token:
Auth0SDK.shared.renewCredentials(refreshToken: savedRefreshToken) { result in
switch result {
case .success(let newCredentials):
print("Token renewed: \(newCredentials.accessToken ?? "")")
case .failure(let error):
print("Renewal failed: \(error)")
}
}Store and retrieve credentials using the platform's secure storage (Keychain on iOS, SharedPreferences on Android):
// Save credentials after login
Auth0SDK.shared.login { result in
if case .success(let credentials) = result {
Auth0SDK.shared.saveCredentials(credentials)
}
}
// Check whether stored credentials are available
if Auth0SDK.shared.hasValidCredentials {
print("User has valid stored credentials")
} else {
print("User needs to log in")
}
// Retrieve stored credentials (auto-renews if expired)
Auth0SDK.shared.getCredentials { result in
switch result {
case .success(let credentials):
print("Retrieved credentials: \(credentials.accessToken ?? "")")
case .failure(let error):
print("Failed to retrieve credentials: \(error)")
}
}
// Clear stored credentials
Auth0SDK.shared.clearCredentials()import SwiftUI
import SkipAuth0
struct LoginView: View {
@State var isLoggedIn = false
@State var userName: String?
@State var errorMessage: String?
var body: some View {
VStack(spacing: 16) {
if isLoggedIn {
if let userName {
Text("Welcome, \(userName)!")
} else {
Text("You are logged in!")
}
Button("Log Out") {
Auth0SDK.shared.logout { result in
if case .success = result {
Auth0SDK.shared.clearCredentials()
isLoggedIn = false
userName = nil
}
}
}
} else {
Text("Please log in")
Button("Log In") {
Auth0SDK.shared.login { result in
switch result {
case .success(let credentials):
Auth0SDK.shared.saveCredentials(credentials)
isLoggedIn = true
// Fetch user profile
if let token = credentials.accessToken {
Auth0SDK.shared.userInfo(accessToken: token) { profileResult in
if case .success(let profile) = profileResult {
userName = profile.name
}
}
}
case .failure(let error):
errorMessage = error.localizedDescription
}
}
}
Button("Sign Up") {
Auth0SDK.shared.signUp(
email: "user@example.com",
password: "securepassword"
) { result in
switch result {
case .success(let credentials):
Auth0SDK.shared.saveCredentials(credentials)
isLoggedIn = true
case .failure(let error):
errorMessage = error.localizedDescription
}
}
}
}
if let errorMessage {
Text(errorMessage).foregroundStyle(.red)
}
}
.padding()
.onAppear {
if Auth0SDK.shared.hasValidCredentials {
Auth0SDK.shared.getCredentials { result in
if case .success = result {
isLoggedIn = true
}
}
}
}
}
}The main singleton for all Auth0 operations.
| Method / Property | Description |
|---|---|
shared |
The singleton instance |
configure(_:) |
Initialize with an Auth0Config |
isConfigured: Bool |
Whether the SDK has been configured |
config: Auth0Config? |
The current configuration |
| Web Auth | |
login(scope:audience:presenting:completion:) |
Start Universal Login (browser) |
logout(federated:presenting:completion:) |
Clear the session |
| Authentication API | |
loginWithCredentials(email:password:scope:audience:completion:) |
Direct email/password login |
signUp(email:password:username:connection:scope:audience:completion:) |
Register a new user and log in |
createUser(email:password:username:connection:completion:) |
Register a new user without logging in |
resetPassword(email:connection:completion:) |
Send a password reset email |
renewCredentials(refreshToken:completion:) |
Refresh an access token |
userInfo(accessToken:completion:) |
Fetch the user profile |
revokeToken(refreshToken:completion:) |
Revoke a refresh token |
| Credentials Manager | |
hasValidCredentials: Bool |
Whether stored credentials exist |
clearCredentials() |
Remove stored credentials |
saveCredentials(_:) -> Bool |
Store credentials in secure storage |
getCredentials(completion:) |
Retrieve stored credentials (auto-renews) |
| Property | Description |
|---|---|
domain: String |
Auth0 tenant domain (e.g. "yourapp.us.auth0.com") |
clientId: String |
OAuth client ID from your Auth0 application |
scheme: String |
URL scheme for callbacks (e.g. "com.example.myapp") |
logoutReturnTo: String? |
Optional custom return-to URL for logout |
| Property | Type | Description |
|---|---|---|
accessToken |
String? |
OAuth2 access token |
idToken |
String? |
OpenID Connect ID token (JWT) |
refreshToken |
String? |
Refresh token for renewing credentials |
tokenType |
String? |
Token type (typically "Bearer") |
expiresAt |
Date? |
When the access token expires |
scope |
String? |
Granted OAuth scopes |
| Property | Type | Description |
|---|---|---|
sub |
String |
The user's unique identifier (the sub claim) |
name |
String? |
Full name |
givenName |
String? |
Given (first) name |
familyName |
String? |
Family (last) name |
nickname |
String? |
Nickname |
email |
String? |
Email address |
emailVerified |
Bool |
Whether the email has been verified |
picture |
String? |
URL of the user's profile picture |
| Property | Type | Description |
|---|---|---|
email |
String |
The user's email address |
username |
String? |
Username (if the connection requires it) |
emailVerified |
Bool |
Whether the email has been verified |
| Case | Description |
|---|---|
.notConfigured |
configure(_:) has not been called |
.missingPresenter |
A platform presenter is required |
.webAuthFailed(String) |
The web authentication flow failed |
.authenticationFailed(String) |
An authentication API call failed |
| Feature | Requirement |
|---|---|
| All features | Auth0 account with a Native application configured |
| Universal Login | Callback URLs configured in Auth0 dashboard for both platforms |
loginWithCredentials |
"Password" grant type enabled in application settings |
signUp / createUser |
"Password" grant type enabled; sign-ups enabled on the database connection |
resetPassword |
A database connection (e.g. Username-Password-Authentication) |
userInfo |
Access token with openid scope |
revokeToken |
A valid refresh token (request offline_access scope during login) |
| iOS | CFBundleURLSchemes registered in Info.plist |
| Android | auth0Domain and auth0Scheme manifest placeholders in skip.yml |
Note
On iOS, the full Auth0.swift API is available since SkipAuth0 re-exports it. The limitations below apply only to the cross-platform API surface.
- Social / OAuth login — Supported through Universal Login (the browser-based flow handles all identity providers configured in your Auth0 dashboard). Direct social token exchange is not wrapped.
- MFA (Multi-Factor Authentication) — Handled by Universal Login when configured in your Auth0 dashboard. Programmatic MFA enrollment/verification is not wrapped.
- Passwordless login (email/SMS codes) — Not yet wrapped. Use the native SDKs directly or Universal Login.
- Organizations — Auth0 Organizations support is available through Universal Login but not wrapped in the cross-platform API.
This project is a Swift Package Manager module that uses the Skip plugin to build the package for both iOS and Android.
Building the module requires that Skip be installed using
Homebrew with brew install skiptools/skip/skip.
This will also install the necessary build prerequisites:
Kotlin, Gradle, and the Android build tools.
The module can be tested using the standard swift test command
or by running the test target for the macOS destination in Xcode,
which will run the Swift tests as well as the transpiled
Kotlin JUnit tests in the Robolectric Android simulation environment.
Parity testing can be performed with skip test,
which will output a table of the test results for both platforms.
This software is licensed under the Mozilla Public License 2.0.