One very cool feature that was pointed out to me was the ability to have defaults or configurations to extend from, shared from a repository. I put mine in the repository Renovate Bot lives in and share it from there.
So, if you find yourself applying the same configuration in multiple repositories, this is maybe something you want to look into.
Where your Renovate Bot lives, I created a defaults.json
, but you can actually call it almost anything you want, you will need remember the name though for when extending it in your config for your repos you are scanning. With the file defaults.json
in place. In this file I put something like this as these are things I keep applying most places:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"prHourlyLimit": 0,
"prConcurrentLimit": 0,
"automerge": true,
"azureWorkItemId": 123456,
"labels": [
"dependencies"
]
}
To use the above defaults.json
it is as easy to remove the configuration entries that you want to use from the defaults and adding a line such as this to your renovate.json
config in the scanned repository.
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["local>MyProjectName/RenovateBot:defaults"],
...
So here I use local>
which means self hosted Git. If you are on GitHub or GitLab or some other hosted Git please refer to the Preset Hosting documentation. For Azure DevOps Repositories, local>
works.
Otherwise, I just specify the Project name in Azure DevOps and the Repository the configuration I want to extend lives in. That is more or less it.
Next time your Renovate Bot runs, it will pull those config items.
There is a lot more you can do along with some recommended presets by the Renovate Bot team which you can apply. Read more about it in their documentation about Sharable Config Presets.
]]>I initially did a couple of things, that I changed later on, after getting a bit more familiar with how Renovate Bot works. I will go through some of the discoveries I did and how I configure it in the end, in this blog post. Hopefully you find this useful 😀
Initially I had the renovate bot configuration and pipeline living in the same repository of the code I wanted it to run against. This is entirely not necessary and it can live in its own repository and have a base configuration, for things such as authentication in this repository.
So now I have a repository called RenovateBot
with two files in it:
azure-pipelines.yml
the pipeline for running the bot (see the previous post on how I set that up)config.js
the configuration for the botWhen running, Renovate already knows how to check out files in the repositories you tell it to scan, in the config file. So you don’t need to run it in the code repositories, you want to scan for updates.
In the config.js
file I now simply have something like:
module.exports = {
hostRules: [
{
hostType: 'nuget',
matchHost: 'https://pkgs.dev.azure.com/myorg/',
username: 'user',
password: process.env.NUGET_TOKEN
},
],
repositories: [
'myorg/repo1',
'myorg/repo2',
'myorg/repo3'
]
};
It will scan all those repositories defined in the repositories
collection.
Neat!
For each repository you define, apart from the basic configuration you provide for renovate, you can add additional configuration. I use this to add tags and group Pull Requests made by Renovate for dependencies that group together. For instance for Unit Test dependencies.
So in each repository you can add a renovate.json
file with additional configuration. This is the same file that Renovate creates initially on a repository on the first Pull Request it makes.
Here is an example of what a configuration for one of my repositories looks like:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"azureWorkItemId": 123456,
"prHourlyLimit": 0,
"labels": ["dependencies"],
"automerge": true,
"packageRules": [
{
"matchPackagePatterns": [
"Cake.*",
"dotnet-sonarscanner",
"dotnet-reportgenerator-globaltool"
],
"groupName": "CI",
"addLabels": [
"CI"
]
},
{
"matchPackagePatterns": [
".*xunit.*",
"Moq.*",
"AutoFixture.*",
"liquidtestreports.*",
"Microsoft.NET.Test.SDK",
"Microsoft.Reactive.Testing",
"MvvmCross.Tests",
"Xamarin.UITest",
"coverlet.*",
"MSTest.*",
"System.IO.Abstractions.*"
],
"groupName": "Unit Test",
"addLabels": [
"Unit Test"
]
}
}
Let’s go through some of the options here.
azureWorkItemId
will add the specific work item Id to every Pull Request it creates. This is especially useful if you have a policy set on your Pull Request to always link a work itemprHourlyLimit
I’ve set this one to 0, such that Renovate Bot can create as many Pull Requests it wants on a repository. Otherwise, I think the default is 2. So if you wonder why it didn’t update all dependencies, this could by whylabels
This option lets you set default labels on pull requests, so for each of my Pull Requests made by Renovate it will have the dependencies
label on itautomerge
This option will set Auto Complete in Azure DevOps on a Pull Request using the default merge strategy, such that you can have Pull Requests automatically merge when all checks are completedpackageRules
Is super powerful. Here you can limit which packages you want to be grouped together, in the case above I have two groups. Unit Test
and CI
, which will look for specific regex patterns of package names to include in the groups. I also add additional labels for these two groups using addLabels
and assign groupName
such that when Renovate creates a Pull Request for a group, the title will be Update <group name>
. There are many more options you can set on packageRules
, you should refer to the docs if you want more info.So far I have scanned .NET projects and Kotlin projects with Renovate Bot and it handles these very well without any issues. I simply add additional repositories in the config.js
file and on next run or when I run the pipeline manually it adds a renovate.json
file to the repository and it is good to go.
When using System.AccessToken
as your Renovate Token, the Pull Requests are opened by the user Project Collection Build Service (myorg)
. This user is built into Azure DevOps and does not have any e-mail assigned to it and you cannot change it either. If you have “Commit author email validation” enabled on a repo, you will need to add both the renovate bot email (or the one you’ve defined in your config) along with the Project Collection user like so: renovate@whitesourcesoftware.com; Project Collection Build Service (myorg)
to the allowed commit author email patterns. Otherwise auto completion on Pull Requests will not work as it will violate one of the repository policies.
Pascal conveniently let me know of a Docker image you can use in your pipelines to run renovate. This docker image comes with the packages pre-installed, such that you just need to execute renovate. This is nice, because then you do not need to install the renovate npm package on every pipeline run.
To configure renovate, you will want to create a config.js
file, here you can add stuff like private NuGet feeds, rules about which labels to apply on PRs and much more. For my usage, I need access too a private NuGet feed, and want to apply a label dependencies
and a work item on every PR that renovate creates:
module.exports = {
hostRules: [
{
hostType: 'nuget',
matchHost: 'https://pkgs.dev.azure.com/<org-name>/',
username: 'user',
password: process.env.NUGET_TOKEN
}
],
repositories: ['<project>/<repository>'],
azureWorkItemId: 12345,
labels: ['dependencies']
};
For private NuGet feeds, you need to add hostRules
, to let renovate know how to authenticate with the NuGet feed. For Azure DevOps Artifacts, you can unfortunately not just use the System.AccessToken
in the pipeline, so you need to create a Personal Access Token (PAT), with permission to read the package feed.
You can have renovate create PRs for one or more repositories, provide a list of repositories you want it to run on. You can quickly deduct this from the URL for your repo, which will be in the format: https://dev.azure.com/<organization>/<project>/_git/<repository>
. Each repository you want to be scanned you add like: <project>/<repository>
.
On my repositories, I have branch protection enabled and have a rule that work items must be linked to each PR. So for this I have created a work item, which I simply use for each renovate bot Pull Request.
That is it for the configuration.
With the configuration in place, you can now set up a pipeline to run renovate based on a schedule. I have used the example renovate suggest, running every night at 3.
This pipeline is using the docker container Pascal Berger let me know exists. So every step after specifying container will run inside of that.
The env
argument NUGET_TOKEN
, is what the password for the hostRule
for the NuGet feed above will be replaced with. In my case it is a Personal Access Token (PAT) that only has access to the private NuGet feed. The GITHUB_COM_TOKEN
is used to get release notes for Pull Request descriptions when renovate creates such.
schedules:
- cron: '0 3 * * *'
displayName: 'Every day at 3am (UTC)'
branches:
include: [develop]
always: true
trigger: none
pool:
vmImage: 'ubuntu-latest'
container: swissgrc/azure-pipelines-renovate:latest
steps:
- bash: |
npx renovate
env:
NUGET_TOKEN: $(NUGET_PAT)
GITHUB_COM_TOKEN: $(GITHUB_TOKEN)
RENOVATE_PLATFORM: azure
RENOVATE_ENDPOINT: $(System.CollectionUri)
RENOVATE_TOKEN: $(System.AccessToken)
With this, you should be good to go! First time renovate runs, it will create a pull request with a renovate.json
file. Merge it and it will from now on create Pull Requests with dependency updates! Neat!
Here is a screenshot of how this looks.
This works in many environments. Refer to the renovate documentation for more info.
]]>I have already migrated every App I work on to NET7.0 and have done multiple releases to stores of these Apps. So wanted to share some insights with everyone.
You maybe we aware that net{6|7|8}.0-ios
targets are incompatible with Xamarin.iOS
targets. The breaking changes primarily are how NFloat
and related types are now handled. Instead of living directly in .NET 6.0, these types are now added implicitly using the NuGet package System.Runtime.InteropServices.NFloat.Internal
. If you are already using .NET 6.0 you might have noticed this package gets pulled in, even though you have no other Package References.
This makes Xamarin.iOS
assemblies not forward compatible with the newer target frameworks. Hence, library authors need to migrate their projects to make them compatible.
I have done a couple of migrations, a huge one in MvvmCross which has a lot of target frameworks it targets. Migrated NukeProxy which is a iOS Binding Library, which had to be converted to using XCFramework instead of a Fat library. I have migrated a bunch of internal libraries and 6 Apps at work. So now I have a little bit of experience behind the belt.
Make sure your .NET version is up to date. As of November .NET 7.0 is out and newer versions of Visual Studio for Mac will pull this the things described here should also be working if even if you are on .NET8.0 already, but also if you are for some reason on .NET6.0.
The bits for Android, iOS, macOS, mac catalyst etc. are no longer distributed with Visual Studio. Instead you will need to use a new feature in .NET to install a “workload” for each of these. This is super easy and much nicer in my opinion, instead of having to open a UI tool you can easily update and install these tools in the command-line. For instance if you want Android and iOS you do:
dotnet workload install android ios
You will need to install the appropriate workload for each Target Framework you intend to support in your library. If you are going to use .NET MAUI, then you might also want to install the
maui maui-android maui-ios
workloads.
The new SDK style project has been available to use for a very long time. The biggest advantage of these new project styles, is that you will not have to specify every single file in your project file that you want to compile. Instead the new style just picks up any know file types and makes some sane assumptions about the build actions for the files and adds them automatically.
This makes working with csproj files much easier, as they are not polluted with loads of <Compile Include="folder/myfile.cs" />
definitions all over the place. The only things you would really have in these new csproj files is defining Target Framework, Package References and Project References. If you have other stuff, you may be doing something wrong.
If you compare the Playground.iOS csproj file in MvvmCross, it went from over 200 lines of code to 29, since the new SDK style projects are so more succinct.
The easiest way to migrate to the new project style and new TFM, is simply creating a new project and dragging over all your files, then adding all the project references and package references.
If you need inspiration of how a csproj file looks you can have some inspiration from some of the MvvmCross project files.
Android Library: MvvmCross RecyclerView csproj
Multi-target Library: MvvmCross Messenger Plugin csproj
In MvvmCross I historically used the excellent MSBuild.SDK.Extras project to help with multi-targeting scenarios. However, after migrating projects to net6.0 I started having weird build issues. Migrating away from MSBuild.SDK.Extras resolved my issues. Your milage may vary, but it has helped me with a bunch of issues not to use it anymore.
If you are reading this and don’t know what it is, then you are not missing out on anything. It was just necessary with the older target frameworks to have a bunch of extra setup, which excellent community members such as Claire Novotny helped making for library authors to have a much nicer experience. However, it appears this is not necessary to use anymore.
If you are using Xamarin.Essentials, you may have heard that this is now moved over to MAUI.Essentials. However, not everyone are making Apps using MAUI, so you don’t really want to pull in everything from MAUI just to have access to the MAUI.Essentials API.
MAUI.Essentials is not a NuGet package you pull in though. As of writing this post you can add MAUI.Essentials, by adding the following to your csproj file in a <PropertyGroup>
:
<UseMauiEssentials>true</UseMauiEssentials>
Remember to initialize MAUI essentials on startup in your Activity or Application or View controller:
Microsoft.Maui.ApplicationModel.Platform.Init(this);
Read more about the migration in the Microsoft Docs
IntPtr
to NativeHandle
on iOSOften when you are creating CollectionView or TableView cells among other views on iOS, you need to add constructors which historically used IntPtr
in the constructor. This has changed now and you need to switch all these over to use NativeHandle
or you will encounter issues at runtime, where it will complain about missing constructors.
There are places in net6.0 and net7.0 where some paths use code that requires JIT compilation. Such as when using parts of System.Linq.Expression which internally uses System.Reflection.Emit. This is supposed to be fixed in net8.0.
Read more here:
If you encounter such issues you will have to add the following to your iOS project:
<UseInterpreter>true</UseInterpreter>
You may also want to experiment with adding
<MtouchInterpreter>-all</MtouchInterpreter>
You can read more about this in Ryan Davis’s excellent blog post about improving build times
But UseInterpreter
kicks in the Interpreter mode for your App and allows for some cool features, which are also described by Ryan Davis in his post about the mono interpreter. Among these emitting code, patching code at runtime and more, but more importantly fixes runtime crashes until libraries get patched.
Business as usual. However, I’ve found that switching over to output XCFramework libraries for the stuff you want to bind, is much easier to work with. Especially if you also want to support Mac Catalyst, then it is a must.
You still need to provide a ApiDefinition and Structs file. Specifically for these you need specific build actions in your csproj file:
<ItemGroup>
<ObjcBindingApiDefinition Include="ApiDefinition.cs" />
<ObjcBindingCoreSource Include="Structs.cs" />
</ItemGroup>
Then you can refer to your XCFrameworks like so:
<ItemGroup>
<NativeReference Include="..\..\Output\NukeProxy.xcframework">
<Kind>Framework</Kind>
<SmartLink>False</SmartLink>
</NativeReference>
<NativeReference Include="..\..\Carthage\Build\Nuke.xcframework">
<Kind>Framework</Kind>
<SmartLink>False</SmartLink>
</NativeReference>
</ItemGroup>
If you are still on .net6.0, you may have issues with BitCode being pulled in for some reason. Refer to this comment on GitHub to see how you can add extra stuff in your csproj to get rid of it. Supposedly it is fixed in .net7.0.
I think this is more or less what you need to know. Sure, this is not a complete migration guide, so you will have to try this yourself, but hopefully there are some things here that might you. If you have any questions, please do reach out on Discord, Mastodon, Twitter or in the comments below.
]]>As you the reader might know I develop mobile Apps with Xamarin and .NET. So this blog post will be geared towards that. However, you would be able to install anything using the same approach.
My colleague pointed out that there is this super cool feature of ZSH called dotfiles. ZSH ships with macOS ans is the shell that you see when you open a terminal. It has a set of very powerful interactive tools but is also a script interpreter. Similar to other shells you might know such as bash, fish and csh.
The configuration for ZSH happens with a file called .zshrc
, which is a list of settings, aliases for commands, styles and more.
Dotfiles in ZSH is a way to organize the settings you would normally put it .zshrc
, but not only that, you can organize other configuration files for other tools, and also organize scripts.
There are multiple attempts to build on top of these dotfiles for easily managing the tools you need for your daily work. The version I went for is Oh Your dotfiles. With this tool you can make a logical folder structure with scripts and dependencies you want to have installed.
Let’s dig into it!
Start off by cloning Oh Your dotfiles:
git clone https://github.com/DanielThomas/oh-your-dotfiles ~/.oh-your-dotfiles
Then you run
ZDOTDIR=~/.oh-your-dotfiles zsh
Followed by:
dotfiles_install
Now you have oh your dotfiles installed. Time to create your own repository to customize your installation. For my own use I’ve created a dotfiles repository, with the tools I need.
I’ve called it dotfiles on purpose as Oh Your Dotfiles automatically looks for paths containing dotfiles
in the name, call it anything you want, just make sure you clone it into a folder starting with .
and containing dotfiles
in your user directory.
Now with the stuff cloned, you can start add content.
For instance. I have a folder called dev
with a script called install.homebrew
with the following contents:
ca-certificates
git
gh
scrcpy
python@3.9
mitmproxy
gawk
imagemagick
curl
wget
tree
This will install all those tools from homebrew.
Similarly if you want to install stuff from homebrew casks you can create a install.homebrew-cask
file and add stuff there. For instance I have this in one of my folders:
firefox
amethyst
anydesk
little-snitch
spotify
gimp
signal
macfuse
keka
hot
When you’ve added some files and stuff to download. Simply run dotfiles_update
and it will install everything.
Remember to synchronize your dotfiles somewhere. Then next time you get a new machine you can simply do:
git clone https://github.com/DanielThomas/oh-your-dotfiles ~/.oh-your-dotfiles
git clone https://github.com/Cheesebaron/dotfiles ~/.dotfiles
ZDOTDIR=~/.oh-your-dotfiles zsh
dotfiles_install
Then you can wait and everything is installed. Super convenient.
]]>net6.0-android
TFM when targeting Android 31. This bug had been fixed in a preview version of the net6.0-android
workload, targeting Android 32 or higher.
So question is, how would you set up your CI or even your local environment to run this?
In my specific case I wanted:
android-Tiramisu
android-33
The steps to install these are fairly simple. Use Android SDK manager to install the platform. Then use dotnet to install the specific workload.
sdkmanager --install "platforms;android-Tiramisu"
dotnet workload install android-33
This enables me to use net6.0-android33.0
as TFM.
sdkmanager
is located in your Android SDK folder to invoke it, you might need to add it to your path or cd into the folder <android-sdk>/cmdline-tools/latest/bin
and run ./sdkmanager
.
Setting up steps in Azure Pipelines is super easy too:
- script: |
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "platforms;android-Tiramisu"
displayName: Install Android SDK for Tiramisu
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.x'
inputs:
version: 6.0.x
- script: |
dotnet workload install android
dotnet workload install android-33
displayName: Install Android Workloads
Now you should be able to build .net6.0-android33.0
TFMs!
The information served in this blog post, may also work with Fiddler and mitm-proxy if you prefer those tools.
Charles Proxy has a way to do SSL proxying, which can show you the text contents of SSL requests and responses. You can specify specific sites to include. To get this to work with Xamarin.Android or .NET6 Android, we need to tell the AndroidClientHandler
which certificates we trust. In addition to the regular set up specifying Charles to be the proxy in the WiFi Settings.
For some reason using the built in Android functionality specific to setting networkSecurityConfig
in the Android manifest and allowing to use trusted user certificates doesn’t work with AndroidClientHandler
, so we have to do some manual grunt work.
First we need to modify the device settings to point towards Charles Proxy. You can do that through WiFi settings on your device. Go to Settings -> Network & Internet -> Wi-Fi. Press the Gear icon next to the connected network and edit it using the pencil icon.
In the Proxy drop down select Manual
Proxy hostname will be the IP of the machine you are running Charles Proxy on. You can find this using Charles in the Help -> Local IP address menu. Proxy port will be the port defined in Charles Proxy settings, default is 8888
Doing this step, you should start seeing traffic flowing into Charles from your device. You will see that all SSL requests/responses have garbled data. Not something you can read. We will fix that in next step.
You can also verify the proxy works by going to http://chls.pro/ssl in a browser on the device, and it will download the Charles Root Certificate. For this setup, it is optional to install it.
Since AndroidClientHandler
doesn’t grab user installed certificates, we need to add them ourselves.
In Charles Proxy go to Help -> SSL Proxying -> Save Charles Root Certificate…. Save it as Binary certificate (.cer).
In your Android project add it to the Assets folder and remember to mark the file Build Action as AndroidAsset.
Now to setting up a AndroidClientHandler
with the certificate.
private AndroidClientHandler CreateClientHandler()
{
CertificateFactory? factory = CertificateFactory.GetInstance("X.509");
var clientHandler = new AndroidClientHandler();
if (factory == null)
return clientHandler;
try
{
using Stream? stream = ApplicationContext?.Assets?.Open("charles-ssl-proxying.cer");
var cert = (X509Certificate?)factory.GenerateCertificate(stream);
if (cert != null)
{
clientHandler.TrustedCerts ??= new List<Certificate>();
clientHandler.TrustedCerts.Add(cert);
}
return clientHandler;
}
catch (Exception e)
{
_logger.LogError(e, "Failed to get Charles Certificate");
throw;
}
}
Basically what happens here is that we load the charles-ssl-proxying
certificate from assets and create a certificate that we add to AndroidClientHandler.TrustedCerts
. This is where it expects extra certificates to be added.
Now with this AndroidClientHandler
you can create your HttpClient
with that handler:
var client = new HttpClient(CreateClientHandler());
In my latest project I started using HttpClientFactory
, which allows me to set this handler once and forget about it.
Anyways, when creating requests with HttpClient
now, you should be albe to see the data sent and received even when using SSL!
]]>Note: remember to add domains to the SSL Proxy Settings in Proxy -> SSL Proxy Settings in Charles. A good practice is to limit these to the domains you expect and want to debug.
All the code in this post can be found on GitHub
A StackOverflow question sparked my interest in playing around with the SafetyNet API on Android.
You might ask, what is this SafetyNet thing? Let us say you are writing an Application, where trust and security is a top concern. You want to verify that the device your App is running on has not been tampered with. It will check both the hardware and the software to see what the condition it is in. Google provides SafetyNet to help with detecting this. It is not bulletproof and there are ways of circumventing this. So it should be kept in mind that it should not be used as the only way to detect and prevent any abuse. There is some very good documentation with a lot more details you can read in the Android Developer docs.
The SafetyNet API can be queried through Google Play Services libraries that you add to your App. This means, the device you are running on will need to have a recent enough version of Google Play Services as well. Devices without Google Play Services, will not be able to pass this check, as such devices are usually not certified by Google.
You will need to add the following NuGet package:
Xamarin.GooglePlayServices.SafetyNet
This package has a ton of dependencies that it pulls in. Additionally if you want to check the response from the API you might also want to install:
System.IdentityModel.Tokens.Jwt
The response is a JWT token that you can verify and its claims will contain the result of the attestation.
Before you can do anything, you need to check if the Device has Google Play Services installed. This is fairly simple and Google provides an easy way to do this through the Play Service libraries that are pulled in as dependencies.
var code = GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(context, 13000000);
The code returned he can be checked for whether the services are available, if not it can also be used to show a message to the user of what they can do to resolve the issue, this can be done like so:
if (code == ConnectionResult.Success)
// bingo!
If we have an error showing a message to the user can be done with:
var instance = GoogleApiAvailability.Instance;
if (instance.IsUserResolvableError(errorCode))
{
instance.ShowErrorDialogFragment(context, errorCode, 4242);
}
This should show a dialog looking like so. But it varies depending on the error:
Once you’ve checked and verified Google Play Services are available, you can then start checking the SafetyNet attestation.
In your Google Cloud Console you will need to enable the Android Device Verification service.
Once you have enabled it, you will be prompted to create an API key. I highly suggest that you create it where you tie it to the application identifier, such that it is only usable by the App. You can create another API key for your validation server. Once you have an API key you are ready to verify your device.
With a API key in your hand and Google Play Services ready and installed, you can now use the SafetyNet client to acquire an attestation.
SafetyNetClient client = SafetyNetClass.GetClient(context);
var nonce = Nonce.Generate(24);
var response = await client.AttestAsync(nonce, attestationApiKey).ConfigureAwait(false);
The nonce
here is very important, the more information it contains, the harder it will be for attackers to make replay attacks on your App. Google recommends you add stuff like:
The example of
nonce
above is not recommended to use
With the response
in your hand, you can already decode it as a JWT token to figure out what SafetyNet thinks about the device:
var result = response.JwsResult;
var jwtToken = new JwtSecurityToken(result);
var cts = jwtToken.Claims.First(claim => claim.Type == "ctsProfileMatch").Value;
var basicIntegrity = jwtToken.Claims.First(claim => claim.Type == "ctsProfileMatch").Value;
The ctsProfileMatch
is a verdict of the device integrity. This will be true
if your device matches a profile of a Google-certified Android device.
While, the basicIntegrity
is more lenient and is telling you whether the device the App is running on has been tampered with.
There is a table in the Android documentation which tells what it means if these values are true
or false
.
Note emulators will always return false for both values
Ideally, you would send this JWT token to be verified on your server to check if the nonce matches and the signatures are valid. Your server would either call Google’s API to validate the JWT token. Which can be done as follows.
public async Task<bool> VerifyAttestationOnline(string attestation)
{
using var request = new HttpRequestMessage(HttpMethod.Post,
$"https://www.googleapis.com/androidcheck/v1/attestations/verify?key={attestationApiKey}");
var data = new JWSRequest { SignedAttestation = attestation };
var json = JsonSerializer.Serialize(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
using var response = await httpClient.SendAsync(request).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
using var responseData = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var attestationResponse = await JsonSerializer.DeserializeAsync<AttestationResponse>(responseData).ConfigureAwait(false);
return attestationResponse.IsValidSignature;
}
public class JWSRequest
{
[JsonPropertyName("signedAttestation")]
public string SignedAttestation { get; set; }
}
public class AttestationResponse
{
[JsonPropertyName("isValidSignature")]
public bool IsValidSignature { get; set; }
}
Alternatively Google provides a sample to validate the JWT token yourself offline.
Make sure you go through [this checklist] before you ship your SafetyNet validation to make sure you have covered all cases.
All the code can be found in a Sample Application I have made and published on GitHub.
]]>So in essence, the App is supposed to open if I hit a URL with a specific pattern for one of our domains. Something like: https://trackman.com/pin/123456
. Where the numbers can differ.
I’ve done this before with a custom scheme like trackman://pin/123456
, which is just a simple entry in the Info.plist for your App. However, for Universal AppLinks, there are many more moving parts involved.
I guess, in order to not be able to open arbitrary URLs in your App. Apple, requires you to host a file called apple-app-site-association
on your Web Server. This file should not have any file extensions. Make sure you also serve it with the Content-Type: application/json
. Also, there should be no redirects to get to this URL.
This file should be in either the root or in a .well-known
folder. For instance:
https://trackman.com/apple-app-site-association
https://trackman.com/.well-known/apple-app-site-association
Apple will check the .well-known
folder first, then fall back to the root.
This check happens when you install the App on your device. There is a process called swcd
that will run and check these associasions on your device during install. If any of this stuff fails, this swcd
process is what you look for in the device log.
The errors will look something like this.
Request for '<private>' for task AASA-80AD262A-3EF6-42A2-B992-AC97234187647 { domain: *.tr....com, bytes: 0, route: cdn } denied because the CDN told us to stop with HTTP Status...
You will need to deduct yourself which domain it relates to, because the console entries redact some of the values. However, it will tell you what is wrong. Some common issues are:
Yes, you read it right. From iOS 14 and up, Apple will hit the file through their CDN. So you have to make sure their CDN can reach the file.
The contents of this JSON file will look as follows. Also refer to Listing 6-1 in these Apple docs. for more details of the format.
{
"applinks": {
"apps": [],
"details": [
{
"appID": "<app id prefix>.<bundle id>",
"paths": [ "/pin/*"]
}
]
}
}
The JSON file format that they suggest in the Supporting Associated Domains documentation, did not work for me! If something doesn’t work, try swiching between the two. Unfortunately Apple does not provide a JSON-Schema for this, so it is not easy to validate if you entered stuff correctly, except that you are returning valid JSON.
The App id prefix and bundle name can be located in the developer portal where you have created your App in the Identifiers section. The App Id will be the same as your Team ID usually. Bundle id, is the Id of your App that you have registered in there. So appID
that goes into the JSON file will look something like: 2V9BB354QZ.my.awesome.app
.
The paths
is an array of paths. So anything that goes after your domain. You can use wildcards like:
*
will match any substring?
will match a single characterExamples:
/tamarin
will match a specific path/cats/*
will match everything in the “cats” section of your website/cats/archives/202?
will match everything in “cats/archives” and 2020-2029You can combine the wildcard characters. The paths
array can have multiple, comma delmited paths for a App Id.
To exclude a path you can write NOT /cats/*
.
Go to Certificates, Identifiers & Profiles in the Apple Developer Portal. Here you will need to enable the “Associated Domains” entitlement for your App.
In the list of Identifiers find your App and Edit it. Enable the “Associated Domains” entitlement
Now go to any Provisioning profiles you have an regenerate them. You might be using Fastlane or something like that. Read the docs for your tool to download or regnerate provisioning profiles.
After regenerating the Provisioning profiles, you might need to update any CI pipeline and machines that you build on with these new profiles.
Entitlements.plist
filesIf you don’t have a Entitlements.plist file, it is now time to create one. Microsoft provides good docs for you to read more about how to create one. You will need to add all the domains you wish your Apps to open. These of course need to match the domains you added in the first step in the apple-app-site-association
file.
This will look something like:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:trackman.com</string>
<string>applinks:ostebaronen.dk</string>
<string>applinks:sub-domain.ostebaronen.dk</string>
</array>
You can also use wildcards here:
<string>applinks:*.ostebaronen.dk</string>
This will match any sub-domain, but not include the raw ostebaronen.dk
domain. If you can avoid it, I recommend not to use the wildcard here. I had issues with the Apple CDN not being able to process the apple-app-site-association
file, since it seems like it expects a wildcard entry in the SSL certificate too. I did not have that. If you are going to use it, make sure to test this thoroughly.
AppDelegate.cs
to handle the URL requests to your AppLuckily it seems like how our App is triggered is that first it launches the App fully. So FinishedLaunching
is allowed to complete. Then ContinueUserActivity
gets triggered with the URL. For good measures I also added an override for UserActivityUpdated
. So in your AppDelegate.cs
override these two methods.
public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
CheckForAppLink(userActivity);
return true;
}
public override void UserActivityUpdated(UIApplication application, NSUserActivity userActivity)
{
CheckForAppLink(userActivity);
}
To get the URL from the NSUserActivity
I got inspired from the AppDelegate.cs
that Xamarin.Forms provides.
The code looks like:
private void CheckForAppLink(NSUserActivity userActivity)
{
var strLink = string.Empty;
if (userActivity.ActivityType == "NSUserActivityTypeBrowsingWeb")
{
strLink = userActivity.WebPageUrl.AbsoluteString;
}
else if (userActivity.UserInfo.ContainsKey(new NSString("link")))
{
strLink = userActivity.UserInfo[new NSString("link")].ToString();
}
// do something clever with your URL here
NavigateToUrl(strLink);
}
This part is a huge pain. First of all, it appears to me, when you deploy an Application using Xcode or Visual Studio, the swcd
process doesn’t run and check the apple-app-site-association
file. This means when you hit your URL in Safari or through a QR code, it will not suggest to open your App.
I had to grab builds from my CI pipeline for this to work 😢.
But things to check:
apple-app-site-association
is reachableEntitlements.plist
match the association json fileswcd
in the search fieldIf you also need to handle custom URL schemes. The only thing you need to do is to register the schemes in your Info.plist
file:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>my.bundle.id</string>
<key>CFBundleURLSchemes</key>
<array>
<string>awesomescheme</string>
<string>tamarin</string>
</array>
</dict>
</array>
Then override OpenUrl
in your AppDelegate.cs
file:
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
if (url.ToString().Contains("/my-pattern"))
{
NavigateToUrl(url.ToString());
return true;
}
return base.OpenUrl(app, url, options);
}
The last alternative, but technically not a part of the API for connecting to WiFi, would be to show a settings panel, where the user could manually enter credentials for a WiFi network in the list.
Note: this code will throw a
ActivityNotFoundException
if run on older Android versions. Make sure to add some version checking.
I guess the folks at Google got a bit backlash or they somehow changed their mind. However, they’ve now added an Intent you can fire to add a Network on the device.
The code for this looks something like this:
var intent = new Intent(
"android.settings.WIFI_ADD_NETWORKS");
var bundle = new Bundle();
bundle.PutParcelableArrayList(
"android.provider.extra.WIFI_NETWORK_LIST",
new List<IParcelable>
{
new WifiNetworkSuggestion.Builder()
.SetSsid(ssid)
.SetWpa2Passphrase(password)
.Build()
});
intent.PutExtras(bundle);
StartActivityForResult(intent, AddWifiSettingsRequestCode);
Note:
AddWifiSettingsRequestCode
is just an Integer you define.
Then in OnActivityResult
you can figure out whether any network in the list you provided was added with:
if (requestCode == AddWifiSettingsRequestCode)
{
if (data != null && data.HasExtra(
"android.provider.extra.WIFI_NETWORK_RESULT_LIST"))
{
var extras =
data.GetIntegerArrayListExtra(
"android.provider.extra.WIFI_NETWORK_RESULT_LIST")
?.Select(i => i.IntValue()).ToArray() ?? new int[0];
if (extras.Length > 0)
{
var ok = extras
.Select(GetResultFromCode)
.All(r => r == Result.Ok);
// if ok is true, BINGO!
return;
}
}
}
The extras will return a list of Result
which indicate whether they were added if it is Result.OK
or not if it is Result.Cancel
.
GetResultFromCode, simply parses the integers returned and turns them into a Result
:
private static Result GetResultFromCode(int code) =>
code switch
{
0 => Result.Ok, // newly added
2 => Result.Ok, // wifi already there
_ => Result.Canceled
};
Running this code will show you a dialog looking something like this
Note: This code works and runs fine on a Google Pixel 3a XL running latest Android 11. However, on my OnePlus 8 running OP8_O2_BETA_3 opening the intent fails, because of OnePlus’s Settings App does not implement the AppCompat theme, crashing. I’ve reported this issue to OnePlus but never heard back from them.
You can check out the code from my Android 11 WiFi Repository on GitHub and have a go testing it yourself.
]]>