This is the sixth part of a series of blog posts related to Azure AD best practices. They are all related to a talk I gave at Tech Days Finland as well as in the Microsoft Identity Developer Community Office Hours.
This time we will look at something that every developer seriously needs to know. Why you should never put secrets in native apps.
When I say native app, I mean a public client in identity-speak. An application that runs in a non-trusted environment. Thus this includes:
- Desktop apps
- Mobile apps (no matter if it's native, React Native, PWA etc.)
- Front-end single page apps
The common thing between all of these is that they all run code on a machine you do not control the access to. If your code runs on a server you own and control, then it's not a public client. It's a confidential client.
The TL;DR is: if the code runs on an untrusted machine, so are the embedded secrets within the code.
You can find a bad example of embedding secrets in the app from the GitHub repo: https://github.com/juunas11/7-deadly-sins-in-azure-ad-app-development/tree/master/SecretsInNativeApp.
The sample app
The app on GitHub is a Windows Forms app that needs to call an API that is protected by Azure AD. In this case the developer received a requirement that users should not be required to log in. So the developer implemented client credentials authentication using a secret embedded within the app.
Here are the constants defined in the app:
private const string Authority = "https://login.microsoftonline.com/your-tenant-id/v2.0"; private const string ClientId = "your-app-client-id"; private const string Secret = "your-app-client-secret"; // DO NOT ACTUALLY DO THIS private const string RedirectUri = "https://localhost"; private const string Scope = "your-app-id-uri-or-client-id/.default"; // e.g. client id + /.default private const string ApiBaseUrl = "https://localhost:44316";
The thing is, this approach works, and some apps probably do this.
But it exposes the secret to pretty much anyone.
Accessing the secret
Now you might be thinking well it isn't that easy to access the secret, you have to be some sort of hacker. That is not the case. Let's see what we need to do.
We'll download a tool called strings. Then you put the compiled .exe file in the same folder with the strings executable. You can also add strings to the PATH to make working with it easier. Then you run it from the command line:
You get quite a lot of output from that, but among it we find:
So I replaced the actual secret with an obviously fake value, but these are the values in the constants! Oops.
That's it. You download one executable from SysInternals and run it on the executable or DLL. It's not hard.
Intercepting the secret
Alternatively we could setup a proxy that intercepts all calls from the app. You could setup e.g. Fiddler on your machine and grab the secret as the app uses it. You can proxy your mobile device's traffic through that as well to intercept secrets from mobile apps.
Single page apps
Doing it better
So how can we do this better? Well, we can't store secrets that are common to the app across users in the app or the untrusted device.
If an API requires authentication, you either have to remove the authentication requirement entirely or partially, or you have to authenticate the user. An API has no way to authenticate the app in this case since it runs in an untrusted environment.
When using an OpenID Connect-compatible identity provider (e.g. Azure AD), you can use the following flows to authenticate the user and acquire access tokens for APIs:
- Desktop app: Authorization Code / Device Code
- Mobile app: Authorization Code
- Front-end single page app: Implicit / Authorization Code with PKCE (not supported in Azure AD for SPAs)
Do not use resource owner password credentials flow, you will lower your security posture.
Don't put secrets in native apps. It is not hard to extract secrets from compiled applications. So if it runs on a machine that you don't control, anyone with access to that machine will have access to the secrets.
Either you authenticate the user, or call APIs that do not require authentication. There is no way to authenticate the app itself as it runs in an untrusted environment.