Tri Nguyen's Blog
Mobile app development, Kotlin, Swift, Xamarin, KMM


1Password Extension for Xamarin.iOS apps

Posted on

1Password is one of my favorite application on Mac and iPhone. It’s working pretty well across devices, browsers (Chrome/Safari in my cases). With one unique master password, 1Password enables different strong, complex passwords to all online socials.

There is a bunch of iOS applications on Store that use 1Password login side already, see here: https://blog.agilebits.com/1password-apps . Most of the popular apps are named here. Below are how it looks on Dropbox and Pluralsight (notice the 1Password icon?)

1Password Extension for Xamarin.iOS apps - Demo Dropbox 1Password Extension for Xamarin.iOS apps - Demo Pluralsight

 

How do we add this extension to our iOS application? There is “1Password Extension for iOS Apps” that helps iOS developers support using 1Password login inside their applications, check out https://github.com/AgileBits/onepassword-app-extension, but only for Objective-C and Swift. Well, how about Xamarin?

Xamarin.iOS Binding for 1Password Extensions

Yes, I made the bindings library for it, check this Nuget package: 1PasswordExtension for Xamarin.iOS

The source code is hosted on Github: https://github.com/trinnguyen/onepassword-app-extension-xamarin

Anyone who is curious about how it is built can look at there and built their own DLL too, it fetches the latest Release of the original Objective-C library to build CocoaTouch framework.

The binding ApiDefinitions class is generated by using Objective Sharpie with CocoaPods components, I hope to have some time to write about it later.

Integrate to 1Password to your Xamarin.iOS app

1. Install Nuget package and add assets

Simply install the Nuget package (bindings of 1Password Extension) to Xamarin.iOS project

Install-Package Xamarin.1PasswordExtension

Add 1Password.xcassets catalog**** to the application bundle, these images are provided by 1Password Team to help user recognize the button. This resource can be found on my Github repo or the original library

2. Add LSApplicationQueriesSchemes in Info.plist

1Password extension uses UIApplication.CanOpenURL to check whether the app extension is available, with value “org-appextension-feature-password-management“, thus any application that supports password management actions can be visible.

One iOS 9+, application need to whitelist org-appextension-feature-password-management in LSApplicationQueriesSchemes

Play with source code

There are 4 use cases that normally integration works, for details check out document of the original library. In this article, I will show how it works on Xamarin with C# code

A demo Xamarin.iOS application is provided on my Github repo, covers 3 use cases. For webView, check below sample code.

check app Extension is available

Hide 1Password button if the App Extension is not visible (1Password app is not installed on iOS device yet)

this.onePasswordButton.Hidden = !OnePasswordExtension.SharedExtension.IsAppExtensionAvailable;

Use Case #1: Native App Login

if (OnePasswordExtension.SharedExtension.IsAppExtensionAvailable)
{
	OnePasswordExtension.SharedExtension.FindLoginForURLString("https://www.acme.com", this, sender, (NSDictionary loginDictionary, NSError error) => 
	{
		if (loginDictionary == null || loginDictionary.Count == 0)
		{
			if (error.Code != AppExtensionErrorCodes.CancelledByUser)
			{
				System.Diagnostics.Debug.WriteLine(@"Error invoking 1Password App Extension for find login: {0}", error);
			}
			return;
		}

		this.usernameTextField.Text = (NSString)loginDictionary[AppExtensionLoginDictionarykeys.UsernameKey];
		this.passwordTextField.Text = (NSString)loginDictionary[AppExtensionLoginDictionarykeys.PasswordKey];
	});	
}

Use Case #2: New User Registration

NSDictionary newLoginDetails = new NSDictionary(
	AppExtensionLoginDictionarykeys.TitleKey, "ACME",
	AppExtensionLoginDictionarykeys.UsernameKey, this.usernameTextField.Text ?? "",
	AppExtensionLoginDictionarykeys.PasswordKey, this.passwordTextField.Text ?? "",
	AppExtensionLoginDictionarykeys.NotesKey, "Saved with the ACME app",
	AppExtensionLoginDictionarykeys.SectionTitleKey, "ACME Browser",
	AppExtensionLoginDictionarykeys.FieldsKey, new NSDictionary("firstname", this.firstnameTextField.Text ?? "",
													"lastname", this.lastnameTextField.Text ?? "")
);

// The password generation options are optional, but are very handy in case you have strict rules about password lengths, symbols and digits.
NSDictionary passwordGenerationOptions = new NSDictionary(
	// The minimum password length can be 4 or more.
	AppExtensionPasswordGeneratorOptions.MinLengthKey, 8,

	// The maximum password length can be 50 or less.
	AppExtensionPasswordGeneratorOptions.MaxLengthKey, 30,

	// If YES, the 1Password will guarantee that the generated password will contain at least one digit (number between 0 and 9). Passing NO will not exclude digits from the generated password.
	AppExtensionPasswordGeneratorOptions.RequireDigitsKey, true,

	// If YES, the 1Password will guarantee that the generated password will contain at least one symbol (See the list bellow). Passing NO with will exclude symbols from the generated password.
	AppExtensionPasswordGeneratorOptions.RequireSymbolsKey, true,

	// Here are all the symbols available in the the 1Password Password Generator:
	// !#$%^&*()_-+=|[]{}'\";.,>?/~`
	// The string for AppExtensionPasswordGeneratorOptions.ForbiddenCharactersKey should contain the symbols and characters that you wish 1Password to exclude from the generated password.
	AppExtensionPasswordGeneratorOptions.ForbiddenCharactersKey, "!#$%/0lIO"
);
OnePasswordExtension.SharedExtension.StoreLoginForURLString("https://www.acme.com", newLoginDetails, passwordGenerationOptions, this, sender, (NSDictionary loginDictionary, NSError error) =>
{
	if (loginDictionary == null || loginDictionary.Count == 0)
	{
		if (error.Code != AppExtensionErrorCodes.CancelledByUser)
		{
			System.Diagnostics.Debug.WriteLine("Failed to use 1Password App Extension to save a new Login: {0}", error);
		}
		return;
	}

	this.usernameTextField.Text = (NSString)loginDictionary[AppExtensionLoginDictionarykeys.UsernameKey] ?? "";
	this.passwordTextField.Text = (NSString)loginDictionary[AppExtensionLoginDictionarykeys.PasswordKey] ?? "";
	this.firstnameTextField.Text = (NSString)((NSDictionary)loginDictionary[AppExtensionLoginDictionarykeys.ReturnedFieldsKey])["firstname"] ?? "";
	this.lastnameTextField.Text = (NSString)((NSDictionary)loginDictionary[AppExtensionLoginDictionarykeys.ReturnedFieldsKey])["lastname"] ?? "";
	// retrieve any additional fields that were passed in newLoginDetails dictionary
});

Use Case #3: Change Password

string changedPassword = this.freshPasswordTextField.Text ?? "";
string oldPassword = this.oldPasswordTextField.Text ?? "";
string confirmationPassword = this.confirmPasswordTextField.Text ?? "";

// Validate that the new password and the old password are not the same.
if (oldPassword.Length > 0 && oldPassword.Equals(changedPassword))
{
	this.ShowChangePasswordFailedAlertWithMessage("The old and the new password must not be the same");
	return;
}

// Validate that the new and confirmation passwords match.
if (!changedPassword.Equals(confirmationPassword))
{
	this.ShowChangePasswordFailedAlertWithMessage("The new passwords and the confirmation password must match");
	return;
}

NSDictionary loginDetails = new NSDictionary(
	AppExtensionLoginDictionarykeys.TitleKey, "ACME", // Optional, used for the third schenario only
	AppExtensionLoginDictionarykeys.UsernameKey, "aUsername", // Optional, used for the third schenario only
	AppExtensionLoginDictionarykeys.PasswordKey, changedPassword,
	AppExtensionLoginDictionarykeys.OldPasswordKey, oldPassword,
	AppExtensionLoginDictionarykeys.NotesKey, "Saved with the ACME app"// Optional, used for the third schenario only
);

// The password generation options are optional, but are very handy in case you have strict rules about password lengths, symbols and digits.
NSDictionary passwordGenerationOptions = new NSDictionary(
	// The minimum password length can be 4 or more.
	AppExtensionPasswordGeneratorOptions.MinLengthKey, 8,

	// The maximum password length can be 50 or less.
	AppExtensionPasswordGeneratorOptions.MaxLengthKey, 30,

	// If YES, the 1Password will guarantee that the generated password will contain at least one digit (number between 0 and 9). Passing NO will not exclude digits from the generated password.
	AppExtensionPasswordGeneratorOptions.RequireDigitsKey, true,

	// If YES, the 1Password will guarantee that the generated password will contain at least one symbol (See the list bellow). Passing NO with will exclude symbols from the generated password.
	AppExtensionPasswordGeneratorOptions.RequireSymbolsKey, true,

	// Here are all the symbols available in the the 1Password Password Generator:
	// !#$%^&*()_-+=|[]{}'\";.,>?/~`
	// The string for AppExtensionPasswordGeneratorOptions.ForbiddenCharactersKey should contain the symbols and characters that you wish 1Password to exclude from the generated password.
	AppExtensionPasswordGeneratorOptions.ForbiddenCharactersKey, "!#$%/0lIO"
);

OnePasswordExtension.SharedExtension.ChangePasswordForLoginForURLString("https://www.acme.com", loginDetails, passwordGenerationOptions, this, sender, (NSDictionary loginDictionary, NSError error) =>
{
	if (loginDictionary == null || loginDictionary.Count == 0)
	{
		if (error.Code != AppExtensionErrorCodes.CancelledByUser)
		{
			System.Diagnostics.Debug.WriteLine("Error invoking 1Password App Extension for find login, {0}", error);
		}
		return;
	}

	this.oldPasswordTextField.Text = (NSString)loginDictionary[AppExtensionLoginDictionarykeys.OldPasswordKey];
	this.freshPasswordTextField.Text = (NSString)loginDictionary[AppExtensionLoginDictionarykeys.PasswordKey];
	this.confirmPasswordTextField.Text = (NSString)loginDictionary[AppExtensionLoginDictionarykeys.PasswordKey];
});

Use Case #4: Web View Filling

OnePasswordExtension.SharedExtension.FillItemIntoWebView(webView, this, sender, false, (bool success, NSError error) => 
{
	if (!success)
	{
		System.Diagnostics.Debug.WriteLine("Failed to fill into webview: {0}", error);
	}	
});