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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="StarKnowledge.OAuth.Maui.OktaSalesforce" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="StarKnowledge.OAuth.Maui.OktaSalesforce" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="StarKnowledge.OAuth.Maui.OktaSalesforce" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add StarKnowledge.OAuth.Maui.OktaSalesforce --version 1.1.0
                    
#r "nuget: StarKnowledge.OAuth.Maui.OktaSalesforce, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package StarKnowledge.OAuth.Maui.OktaSalesforce@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=StarKnowledge.OAuth.Maui.OktaSalesforce&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=StarKnowledge.OAuth.Maui.OktaSalesforce&version=1.1.0
                    
Install as a Cake Tool

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 calls
  • ACCESS_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 IdpLogoutUrl is 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)

  1. Revoke Salesforce tokens — POST to Salesforce's OAuth revoke endpoint to invalidate the access and refresh tokens server-side.
  2. Clear local session data — Removes these secure-storage keys:
    • SalesforceOAuthAccessToken
    • SalesforceOAuthRefreshToken
    • SalesforceOAuthInstanceUrl
  3. Clear browser session cookies (Android only):
    • Opens {instanceUrl}/secur/logout.jsp in a Chrome Custom Tab to clear the Salesforce session cookie.
    • Opens each IdpLogoutUrl in a Chrome Custom Tab to clear the IDP SSO cookie.
    • The user closes each Custom Tab to continue.

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:

  1. Go to Okta Admin Console
  2. Navigate to Applications ? Applications
  3. Find your Salesforce application
  4. 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:

  1. Go to Azure Portal
  2. Navigate to Azure Active Directory ? App registrations
  3. Create a new application registration
  4. Copy your Tenant ID and Client ID
  5. Create a client secret under Certificates & secrets
  6. 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:

  1. Go to Salesforce Setup
  2. Navigate to Apps ? App Manager
  3. Create a New Connected App
  4. Enable OAuth Settings
  5. Add Redirect URI: yourapp://oauth2redirect/callback
  6. 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: SecureStorage from 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.jsp in 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:

  1. Verify the OTP hasn't expired (typically 5-10 minutes)
  2. Check your email for the correct OTP
  3. Ensure the device token matches

"Invalid authorization response" on iOS

Solution:

  1. Verify URL scheme is registered in Info.plist
  2. Test manually: Open Safari and type yourapp://oauth2redirect/callback
  3. 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:

  1. Verify your Client ID and Client Secret are correct
  2. Check that your callback URL is registered correctly
  3. Ensure the OAuth provider's redirect URI matches exactly
  4. Verify your app has the necessary permissions/scopes

Android: Authentication Flow Not Working

Solution:

  1. Ensure WebAuthenticationCallbackActivity is configured in AndroidManifest.xml
  2. Verify callback URL scheme matches your app configuration
  3. Check that WebAuthenticator is available in your MAUI setup
  4. Verify Exported = true attribute is set on the activity
  5. Check that IntentFilter schemes match your callback URLs

?? Additional Resources


?? 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 WebAuthenticationCallbackActivity to AndroidManifest.xml
  • Create WebAuthenticationCallbackActivity.cs class
  • Add [IntentFilter] attributes for each callback scheme
  • Add INTERNET and ACCESS_NETWORK_STATE permissions
  • Verify Exported = true attribute

Application Setup

  • Register services in MauiProgram.cs
  • Call InitializeConfigData() to load configuration
  • Call InitializeAsync() on ISalesforceService
  • Inject ISalesforceService into 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() after LoginWithOAuth2(): token cleared, browser opens Salesforce logout, app returns to login UI
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 138 3/27/2026
1.0.1 108 3/26/2026
1.0.0 122 3/4/2026