StarKnowledge.OAuth.Maui.OktaSalesforce
1.1.0
dotnet add package StarKnowledge.OAuth.Maui.OktaSalesforce --version 1.1.0
NuGet\Install-Package StarKnowledge.OAuth.Maui.OktaSalesforce -Version 1.1.0
<PackageReference Include="StarKnowledge.OAuth.Maui.OktaSalesforce" Version="1.1.0" />
<PackageVersion Include="StarKnowledge.OAuth.Maui.OktaSalesforce" Version="1.1.0" />
<PackageReference Include="StarKnowledge.OAuth.Maui.OktaSalesforce" />
paket add StarKnowledge.OAuth.Maui.OktaSalesforce --version 1.1.0
#r "nuget: StarKnowledge.OAuth.Maui.OktaSalesforce, 1.1.0"
#:package StarKnowledge.OAuth.Maui.OktaSalesforce@1.1.0
#addin nuget:?package=StarKnowledge.OAuth.Maui.OktaSalesforce&version=1.1.0
#tool nuget:?package=StarKnowledge.OAuth.Maui.OktaSalesforce&version=1.1.0
Okta & Salesforce OAuth MAUI Package
A ready-to-use .NET MAUI NuGet package for OAuth 2.0 authentication. Easily integrate Okta, Azure AD, or any OAuth 2.0 provider with Salesforce into your MAUI applications with just a few lines of code.
?? Quick Start
Step 1: Install the Package
Add the package to your MAUI project:
Using Package Manager:
Install-Package StarKnowledge.OAuth.Maui.OktaSalesforce
Using .NET CLI:
dotnet add package StarKnowledge.OAuth.Maui.OktaSalesforce
Or add to your .csproj file:
<PackageReference Include="StarKnowledge.OAuth.Maui.OktaSalesforce" Version="1.0.0" />
Step 2: Register Services in MauiProgram.cs
Open your MauiProgram.cs file and register the services:
using Microsoft.Extensions.Logging;
using Xamarin.Nuget.Okta.SF.CS;
using Xamarin.Nuget.Okta.SF.CS.Services;
namespace YourApp
{
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// ? Register services for dependency injection
builder.Services.AddSingleton<ISecureStorages, SecureStorageWrapper>();
builder.Services.AddSingleton<ISalesforceService, SalesforceService>();
builder.Services.AddTransient<MainPage>();
#if DEBUG
builder.Logging.AddDebug();
#endif
var app = builder.Build();
// ? Initialize configuration data asynchronously
InitializeConfigData(app.Services);
return app;
}
private static async void InitializeConfigData(IServiceProvider services)
{
try
{
var secureStorage = services.GetRequiredService<ISecureStorages>();
// ========================================
// Choose Your OAuth Provider
// ========================================
// Option 1: Okta Configuration
await ConfigureOktaAsync(secureStorage);
// Option 2: Azure AD Configuration (Uncomment to use)
// await ConfigureAzureAdAsync(secureStorage);
// ========================================
// Salesforce OAuth 2.0 Configuration
// ========================================
await ConfigureSalesforceAsync(secureStorage);
// Initialize the SalesforceService with configuration values
var salesforceService = services.GetRequiredService<ISalesforceService>();
await salesforceService.InitializeAsync();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error initializing config data: {ex.Message}");
}
}
// ========================================
// OAuth Provider Configurations
// ========================================
private static async Task ConfigureOktaAsync(ISecureStorages secureStorage)
{
// Okta Domain Configuration
string oktaDomain = "https://your-company.okta.com"; // Replace with your Okta domain
await secureStorage.SetAsync("OktaDomain", oktaDomain + "/api/v1/authn");
await secureStorage.SetAsync("IdentityProviderLoginURL",
"https://your-company.okta.com/app/salesforce/YOUR_OKTA_APP_ID/sso/saml");
await secureStorage.SetAsync("AuthUrl",
oktaDomain + "/api/v1/authn/factors/{0}/verify");
await secureStorage.SetAsync("OktaAppId", "YOUR_OKTA_APP_ID");
}
private static async Task ConfigureAzureAdAsync(ISecureStorages secureStorage)
{
// Azure AD Configuration
string tenantId = "YOUR_TENANT_ID";
string clientId = "YOUR_AZURE_CLIENT_ID";
string clientSecret = "YOUR_AZURE_CLIENT_SECRET";
await secureStorage.SetAsync("AzureTenantId", tenantId);
await secureStorage.SetAsync("AzureClientId", clientId);
await secureStorage.SetAsync("AzureClientSecret", clientSecret);
await secureStorage.SetAsync("AzureAuthorityUrl",
$"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize");
await secureStorage.SetAsync("AzureTokenUrl",
$"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token");
await secureStorage.SetAsync("AzureScopes", "User.Read offline_access");
}
private static async Task ConfigureSalesforceAsync(ISecureStorages secureStorage)
{
// Salesforce Configuration
await secureStorage.SetAsync("LoginEndpoint",
"https://login.salesforce.com/services/oauth2/token");
await secureStorage.SetAsync("ApiEndpoint", "/services/data/v59.0/");
await secureStorage.SetAsync("InstanceUrl",
"https://YOUR_INSTANCE.salesforce.com/");
// Salesforce OAuth 2.0 Configuration
await secureStorage.SetAsync("SalesforceAuthorizeUrl",
"https://login.salesforce.com/services/oauth2/authorize");
await secureStorage.SetAsync("SalesforceClientId",
"YOUR_SALESFORCE_CLIENT_ID");
await secureStorage.SetAsync("SalesforceRedirectUri",
"yourapp://oauth2redirect/callback");
await secureStorage.SetAsync("SalesforceTokenEndpoint",
"https://login.salesforce.com/services/oauth2/token");
await secureStorage.SetAsync("SalesforceClientSecret",
"YOUR_SALESFORCE_CLIENT_SECRET");
}
}
}
Step 3: Use in Your Code
In any page or view model, inject and use the service:
using Xamarin.Nuget.Okta.SF.CS;
public partial class LoginPage : ContentPage
{
private readonly ISalesforceService _salesforceService;
public LoginPage(ISalesforceService salesforceService)
{
InitializeComponent();
_salesforceService = salesforceService;
}
private async void LoginButton_Clicked(object sender, EventArgs e)
{
try
{
// Option 1: Login with username and password (Okta with MFA support)
var result = await _salesforceService.Login("user@company.com", "password");
// Option 2: OAuth 2.0 Login (Works with both Okta and Azure AD)
// var result = await _salesforceService.LoginWithOAuth2();
if (result.Contains("Access Token:"))
{
var accessToken = result.Replace("Access Token: ", "");
await DisplayAlert("Success", "Logged in successfully!", "OK");
// Now use the token to call Salesforce APIs
await CallSalesforceApi(accessToken);
}
else
{
await DisplayAlert("Error", result, "OK");
}
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.Message, "OK");
}
}
private async Task CallSalesforceApi(string accessToken)
{
try
{
var apiResult = await _salesforceService.CallSalesForceApi(accessToken);
await DisplayAlert("Salesforce Data", apiResult, "OK");
}
catch (Exception ex)
{
await DisplayAlert("Error", $"API call failed: {ex.Message}", "OK");
}
}
}
Step 4: Configure iOS (Required for iOS apps)
Add this to your Platforms/iOS/Info.plist file:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.yourcompany.yourapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yourapp</string>
</array>
</dict>
</array>
Step 4b: Configure Android (Required for Android apps)
1. Update AndroidManifest.xml
Add the WebAuthenticationCallbackActivity to your Platforms/Android/AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">
<activity android:name=".WebAuthenticationCallbackActivity" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
Required Permissions:
INTERNET: Required for OAuth authentication and API callsACCESS_NETWORK_STATE: Required to check network connectivity
For API Level 30+, add this optional <queries> section to improve browser/Custom Tabs discovery:
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
2. Create WebAuthenticationCallbackActivity
Create a file at Platforms/Android/WebAuthenticationCallbackActivity.cs:
using Android.App;
using Android.Content;
using Android.Content.PM;
using Microsoft.Maui.Authentication;
namespace MauiApp1.Platforms.Android
{
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = "salesforceoauth")]
public class WebAuthenticationCallbackActivity : WebAuthenticatorCallbackActivity
{
// Handles OAuth 2.0 authentication callbacks from the browser
// Callback URL: salesforceoauth://callback
}
}
To customize for your callback URL:
If your callback URL is yourapp://oauth2redirect/callback, use:
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = "yourapp")]
If your callback URL is https://www.example.com/oauth/callback, use:
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = "https",
DataHost = "www.example.com",
DataPathPrefix = "/oauth/callback")]
Activity Attributes Explained:
| Attribute | Purpose |
|-----------|---------|
| NoHistory = true | Activity won't appear in back stack after callback |
| LaunchMode = LaunchMode.SingleTop | Reuses existing instance if available |
| Exported = true | Critical: Activity can be invoked by other apps (required for OAuth callbacks) |
IntentFilter Attributes:
| Attribute | Purpose |
|-----------|---------|
| DataScheme | The URL scheme (e.g., yourapp, salesforceoauth) |
| DataHost | Domain for HTTPS callbacks (optional) |
| DataPathPrefix | Path prefix for HTTPS callbacks (optional) |
Important: Use only one [IntentFilter] that matches your actual callback URL. Do not add multiple IntentFilters unless you support multiple callback URLs.
Step 5: Register Callback URL
Register the following callback URL in your OAuth provider and Salesforce:
Callback URL: yourapp://oauth2redirect/callback
?? Android Callback URL Configuration Guide
When you set your callback URL in MauiProgram.cs, you must also configure the matching [IntentFilter] in WebAuthenticationCallbackActivity.cs.
Quick Reference
If your callback URL is:
myapp://callback
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = "myapp")]
salesforceoauth://oauth2redirect
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = "salesforceoauth")]
https://mycompany.com/oauth/callback
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = "https",
DataHost = "mycompany.com",
DataPathPrefix = "/oauth/callback")]
Rule: Extract the scheme from your callback URL and use it in DataScheme. For HTTPS URLs, also provide DataHost and DataPathPrefix.
?? Features
Supported OAuth Providers
1. Okta ?
- ? Username/Password authentication
- ? Multi-Factor Authentication (MFA) via email OTP
- ? SAML authentication with Salesforce
- ? OAuth 2.0 authentication flow
2. Azure AD / Microsoft Entra ID ?
- ? OAuth 2.0 authentication flow
- ? Conditional Access support
- ? Multi-tenant support
- ? Offline access token support
Authentication Methods
1. Username/Password Authentication (Okta with MFA Support)
await _salesforceService.InitializeAsync();
var result = await _salesforceService.Login("username", "password");
// If MFA is enabled, you'll receive an OTP
if (result.Contains("OTP sent"))
{
// Get the OTP from user and verify
var accessTokenResult = await _salesforceService.GetAccessToken(otp, deviceToken);
}
2. OAuth 2.0 Authentication (Okta or Azure AD)
await _salesforceService.InitializeAsync();
var result = await _salesforceService.LoginWithOAuth2();
if (result.Contains("Access Token:"))
{
var accessToken = result.Replace("Access Token: ", "");
// Use the token for API calls
}
Salesforce API Calls
var accessToken = "your-access-token";
var result = await _salesforceService.CallSalesForceApi(accessToken);
// Result will contain your Salesforce data (e.g., Account information)
var accounts = JsonConvert.DeserializeObject(result);
Sign out (Logout)
LogoutAsync() performs a complete sign-out that clears both the Salesforce session and the SSO identity provider session (Okta, Azure AD, Ping, OneLogin, ADFS, or any OIDC/SAML IDP).
Configuration
Set the IdpLogoutUrl in your startup configuration to enable full SSO logout:
// Single IDP
await secureStorage.SetAsync("IdpLogoutUrl",
"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/logout");
// Multiple IDPs (comma-separated)
await secureStorage.SetAsync("IdpLogoutUrl",
"https://your-org.okta.com/login/signout, https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/logout");
Common IDP Logout URLs:
| IDP | Logout URL |
|---|---|
| Azure AD | https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/logout |
| Okta | https://your-org.okta.com/login/signout |
| Ping Identity | https://sso.example.com/idp/startSLO.ping |
| OneLogin | https://your-org.onelogin.com/oidc/2/logout |
Note: If
IdpLogoutUrlis not set,LogoutAsync()will still revoke Salesforce tokens and clear stored session data — it just won't clear the IDP's SSO cookie.
Basic Usage
await _salesforceService.LogoutAsync();
Sample: Sign-out with Loading Overlay
Wire a Sign out button with a loading overlay for better UX on Android:
MainPage.xaml:
<Grid>
<ScrollView>
<VerticalStackLayout>
<Button x:Name="btnLogout" Text="Sign out" Clicked="LogoutButton_Clicked" />
</VerticalStackLayout>
</ScrollView>
<Grid x:Name="signoutOverlay" IsVisible="False" BackgroundColor="#CC000000"
VerticalOptions="Fill" HorizontalOptions="Fill">
<VerticalStackLayout VerticalOptions="Center" HorizontalOptions="Center" Spacing="15">
<ActivityIndicator IsRunning="True" Color="White" HeightRequest="50" WidthRequest="50" />
<Label Text="Signing out..." TextColor="White" FontSize="18" HorizontalOptions="Center" />
<Label Text="Please close the browser tab to continue"
TextColor="#AAAAAA" FontSize="13" HorizontalOptions="Center" />
</VerticalStackLayout>
</Grid>
</Grid>
MainPage.xaml.cs:
private async void LogoutButton_Clicked(object sender, EventArgs e)
{
try
{
// Show overlay and disable buttons
signoutOverlay.IsVisible = true;
btnLogin.IsEnabled = false;
btnLogout.IsEnabled = false;
await _salesforceService.LogoutAsync();
// Clear local state
_accessToken = null;
signoutOverlay.IsVisible = false;
btnLogin.IsEnabled = true;
btnLogout.IsEnabled = true;
await DisplayAlert("Signed out", "You have been signed out.", "OK");
if (Shell.Current != null)
await Shell.Current.GoToAsync("///MainPage");
}
catch (Exception ex)
{
signoutOverlay.IsVisible = false;
btnLogin.IsEnabled = true;
btnLogout.IsEnabled = true;
await DisplayAlert("Sign out", ex.Message, "OK");
}
}
What LogoutAsync Does (in order)
- Revoke Salesforce tokens —
POSTto Salesforce's OAuth revoke endpoint to invalidate the access and refresh tokens server-side. - Clear local session data — Removes these secure-storage keys:
SalesforceOAuthAccessTokenSalesforceOAuthRefreshTokenSalesforceOAuthInstanceUrl
- Clear browser session cookies (Android only):
- Opens
{instanceUrl}/secur/logout.jspin a Chrome Custom Tab to clear the Salesforce session cookie. - Opens each
IdpLogoutUrlin a Chrome Custom Tab to clear the IDP SSO cookie. - The user closes each Custom Tab to continue.
- Opens
Platform-Specific Behavior
| Platform | Login | Logout |
|---|---|---|
| iOS | ASWebAuthenticationSession with PrefersEphemeralWebBrowserSession = true — each login is a completely fresh session with no cached cookies. IDP always shows its login page. |
No browser tabs needed. Token revoke + clear storage only. |
| Android | Chrome Custom Tabs (ignores PrefersEphemeralWebBrowserSession) — shares Chrome's full cookie jar. |
Opens Custom Tabs for SF + IDP logout to clear cookies. User closes each tab. |
Why Android shows browser tabs during logout: Chrome Custom Tabs share Chrome's cookie store, and Android provides no API to clear Chrome's cookies programmatically. The only way to clear SSO cookies is to navigate to the logout page in Chrome itself. This is the industry-standard approach used by enterprise apps.
Instance URL after OAuth Login
The token response's instance_url is persisted when LoginWithOAuth2() succeeds:
var instanceUrl = await _salesforceService.GetOAuthInstanceUrlAsync();
?? Configuration Guide
Okta Configuration
// Required: Your Okta domain
string oktaDomain = "https://your-company.okta.com";
await secureStorage.SetAsync("OktaDomain", oktaDomain + "/api/v1/authn");
await secureStorage.SetAsync("IdentityProviderLoginURL",
"https://your-company.okta.com/app/salesforce/YOUR_APP_ID/sso/saml");
await secureStorage.SetAsync("AuthUrl",
oktaDomain + "/api/v1/authn/factors/{0}/verify");
await secureStorage.SetAsync("OktaAppId", "YOUR_OKTA_APP_ID");
Get your credentials from: Okta Admin Console
Steps to get Okta App ID:
- Go to Okta Admin Console
- Navigate to Applications ? Applications
- Find your Salesforce application
- Copy the App ID from the application settings
Azure AD Configuration
string tenantId = "YOUR_TENANT_ID";
string clientId = "YOUR_AZURE_CLIENT_ID";
string clientSecret = "YOUR_AZURE_CLIENT_SECRET";
await secureStorage.SetAsync("AzureTenantId", tenantId);
await secureStorage.SetAsync("AzureClientId", clientId);
await secureStorage.SetAsync("AzureClientSecret", clientSecret);
await secureStorage.SetAsync("AzureAuthorityUrl",
$"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize");
await secureStorage.SetAsync("AzureTokenUrl",
$"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token");
await secureStorage.SetAsync("AzureScopes", "User.Read offline_access");
Get your credentials from: Azure Portal - App Registrations
Steps to get Azure AD Credentials:
- Go to Azure Portal
- Navigate to Azure Active Directory ? App registrations
- Create a new application registration
- Copy your Tenant ID and Client ID
- Create a client secret under Certificates & secrets
- Register your callback URL under Authentication ? Redirect URIs
Salesforce Configuration
// Salesforce API Configuration
await secureStorage.SetAsync("LoginEndpoint",
"https://login.salesforce.com/services/oauth2/token");
await secureStorage.SetAsync("InstanceUrl",
"https://YOUR_INSTANCE.salesforce.com/");
await secureStorage.SetAsync("ApiEndpoint", "/services/data/v59.0/");
// Salesforce OAuth 2.0 Configuration
await secureStorage.SetAsync("SalesforceAuthorizeUrl",
"https://login.salesforce.com/services/oauth2/authorize");
await secureStorage.SetAsync("SalesforceClientId",
"YOUR_SALESFORCE_CLIENT_ID");
await secureStorage.SetAsync("SalesforceRedirectUri",
"yourapp://oauth2redirect/callback");
await secureStorage.SetAsync("SalesforceTokenEndpoint",
"https://login.salesforce.com/services/oauth2/token");
await secureStorage.SetAsync("SalesforceClientSecret",
"YOUR_SALESFORCE_CLIENT_SECRET");
Get your credentials from: Salesforce Setup
Steps to create a Connected App:
- Go to Salesforce Setup
- Navigate to Apps ? App Manager
- Create a New Connected App
- Enable OAuth Settings
- Add Redirect URI:
yourapp://oauth2redirect/callback - Copy your Client ID and Client Secret
?? SecureStorage Wrapper
The package includes a SecureStorageWrapper for safely storing sensitive configuration data:
public interface ISecureStorages
{
Task<string> GetAsync(string key);
Task SetAsync(string key, string value);
string[] GetScopes();
}
public class SecureStorageWrapper : ISecureStorages
{
// Dictionary-based implementation for testing
// Can be replaced with platform-specific secure storage
private readonly Dictionary<string, string> _storage = new Dictionary<string, string>();
// ...implementation
}
In Production, consider replacing with:
- iOS: Keychain
- Android: Android KeyStore
- MAUI:
SecureStoragefrom MAUI Essentials
? What the Package Handles for You
- ? Authenticates with Okta (username/password and OAuth 2.0)
- ? Authenticates with Azure AD (OAuth 2.0)
- ? Supports Okta MFA via email OTP
- ? SAML authentication with Salesforce
- ? OAuth 2.0 authorization code flow
- ? Secure token storage and retrieval
- ? API calls to Salesforce
- ? OAuth 2.0 sign-out: token revoke, clear stored session, Salesforce
secur/logout.jspin browser - ? Cross-platform support (iOS, Android, Mac Catalyst)
- ? Native iOS authentication (ASWebAuthenticationSession)
- ? Android WebAuthenticator support
?? Platform Support
| Platform | Version | Status |
|---|---|---|
| iOS | 12.0+ | ? Fully Supported (ASWebAuthenticationSession) |
| Android | API 21+ | ? Fully Supported (WebAuthenticator) |
| Mac Catalyst | 15.0+ | ? Fully Supported |
?? Complete Example - Okta + Salesforce
MauiProgram.cs
using Microsoft.Extensions.Logging;
using Xamarin.Nuget.Okta.SF.CS;
using Xamarin.Nuget.Okta.SF.CS.Services;
namespace YourApp
{
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
builder.Services.AddSingleton<ISecureStorages, SecureStorageWrapper>();
builder.Services.AddSingleton<ISalesforceService, SalesforceService>();
builder.Services.AddTransient<MainPage>();
#if DEBUG
builder.Logging.AddDebug();
#endif
var app = builder.Build();
InitializeConfigData(app.Services);
return app;
}
private static async void InitializeConfigData(IServiceProvider services)
{
try
{
var secureStorage = services.GetRequiredService<ISecureStorages>();
// Configure Okta
string oktaDomain = "https://your-company.okta.com";
await secureStorage.SetAsync("OktaDomain", oktaDomain + "/api/v1/authn");
await secureStorage.SetAsync("IdentityProviderLoginURL",
"https://your-company.okta.com/app/salesforce/YOUR_APP_ID/sso/saml");
await secureStorage.SetAsync("AuthUrl",
oktaDomain + "/api/v1/authn/factors/{0}/verify");
// Configure Salesforce
await secureStorage.SetAsync("LoginEndpoint",
"https://login.salesforce.com/services/oauth2/token");
await secureStorage.SetAsync("InstanceUrl",
"https://YOUR_INSTANCE.salesforce.com/");
await secureStorage.SetAsync("ApiEndpoint", "/services/data/v59.0/");
await secureStorage.SetAsync("SalesforceAuthorizeUrl",
"https://login.salesforce.com/services/oauth2/authorize");
await secureStorage.SetAsync("SalesforceClientId", "YOUR_CLIENT_ID");
await secureStorage.SetAsync("SalesforceRedirectUri", "yourapp://oauth2redirect/callback");
await secureStorage.SetAsync("SalesforceTokenEndpoint",
"https://login.salesforce.com/services/oauth2/token");
await secureStorage.SetAsync("SalesforceClientSecret", "YOUR_CLIENT_SECRET");
var salesforceService = services.GetRequiredService<ISalesforceService>();
await salesforceService.InitializeAsync();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error: {ex.Message}");
}
}
}
}
MainPage.xaml.cs
using Xamarin.Nuget.Okta.SF.CS;
public partial class MainPage : ContentPage
{
private readonly ISalesforceService _salesforceService;
public MainPage(ISalesforceService salesforceService)
{
InitializeComponent();
_salesforceService = salesforceService;
}
private async void OktaLoginButton_Clicked(object sender, EventArgs e)
{
try
{
var result = await _salesforceService.Login("user@company.com", "password");
if (result.Contains("OTP sent"))
{
await DisplayAlert("MFA Required", "Enter OTP sent to your email", "OK");
}
else if (result.Contains("Access Token:"))
{
var token = result.Replace("Access Token: ", "");
await DisplayAlert("Success", "Logged in with Okta!", "OK");
}
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.Message, "OK");
}
}
private async void OAuth2LoginButton_Clicked(object sender, EventArgs e)
{
try
{
var result = await _salesforceService.LoginWithOAuth2();
if (result.Contains("Access Token:"))
{
var token = result.Replace("Access Token: ", "");
await DisplayAlert("Success", "OAuth2 login successful!", "OK");
}
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.Message, "OK");
}
}
}
? Troubleshooting
"InitializeAsync must be called first"
Solution: Always call await _salesforceService.InitializeAsync() before using authentication methods.
"OTP sent but verification fails"
Solution:
- Verify the OTP hasn't expired (typically 5-10 minutes)
- Check your email for the correct OTP
- Ensure the device token matches
"Invalid authorization response" on iOS
Solution:
- Verify URL scheme is registered in
Info.plist - Test manually: Open Safari and type
yourapp://oauth2redirect/callback - Ensure callback URL matches in all three places:
- Your app's callback URL
- Okta/Azure AD configuration
- Salesforce Connected App configuration
"Access Token Exchange Failed"
Solution:
- Verify your Client ID and Client Secret are correct
- Check that your callback URL is registered correctly
- Ensure the OAuth provider's redirect URI matches exactly
- Verify your app has the necessary permissions/scopes
Android: Authentication Flow Not Working
Solution:
- Ensure
WebAuthenticationCallbackActivityis configured inAndroidManifest.xml - Verify callback URL scheme matches your app configuration
- Check that
WebAuthenticatoris available in your MAUI setup - Verify
Exported = trueattribute is set on the activity - Check that IntentFilter schemes match your callback URLs
?? Additional Resources
- Okta Documentation
- Azure AD Authentication Flow
- Salesforce Connected Apps
- MAUI WebAuthenticator
- Android Intent Filters
- Android Deep Links
?? Complete Setup Checklist
Before You Start
- Have Okta tenant or Azure AD configured
- Create Salesforce Connected App
- Generate Client ID and Client Secret
iOS Setup
- Add URL scheme to
Info.plist - Register callback URL in Okta/Azure AD
- Register callback URL in Salesforce
Android Setup
- Add
WebAuthenticationCallbackActivitytoAndroidManifest.xml - Create
WebAuthenticationCallbackActivity.csclass - Add
[IntentFilter]attributes for each callback scheme - Add INTERNET and ACCESS_NETWORK_STATE permissions
- Verify
Exported = trueattribute
Application Setup
- Register services in
MauiProgram.cs - Call
InitializeConfigData()to load configuration - Call
InitializeAsync()onISalesforceService - Inject
ISalesforceServiceinto your pages
Testing
- Test login flow on iOS device/simulator
- Test login flow on Android device/emulator
- Verify tokens are returned correctly
- Test API calls with returned tokens
- Test
LogoutAsync()afterLoginWithOAuth2(): token cleared, browser opens Salesforce logout, app returns to login UI
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0-android35.0 is compatible. net9.0-ios18.0 is compatible. net9.0-maccatalyst18.0 is compatible. net10.0-android was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. |
-
net9.0-android35.0
- HtmlAgilityPack (>= 1.11.54)
- Microsoft.AspNetCore.Http (>= 2.2.2)
- Microsoft.AspNetCore.Http.Abstractions (>= 2.2.0)
- Microsoft.Maui.Controls (>= 8.0.100)
- Microsoft.Maui.Essentials (>= 8.0.100)
- Newtonsoft.Json (>= 13.0.3)
-
net9.0-ios18.0
- HtmlAgilityPack (>= 1.11.54)
- Microsoft.AspNetCore.Http (>= 2.2.2)
- Microsoft.AspNetCore.Http.Abstractions (>= 2.2.0)
- Microsoft.Maui.Controls (>= 8.0.100)
- Microsoft.Maui.Essentials (>= 8.0.100)
- Newtonsoft.Json (>= 13.0.3)
-
net9.0-maccatalyst18.0
- HtmlAgilityPack (>= 1.11.54)
- Microsoft.AspNetCore.Http (>= 2.2.2)
- Microsoft.AspNetCore.Http.Abstractions (>= 2.2.0)
- Microsoft.Maui.Controls (>= 8.0.100)
- Microsoft.Maui.Essentials (>= 8.0.100)
- Newtonsoft.Json (>= 13.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.