The
OAuth 2.0 for Native Apps spec represents the best practices for OAuth 2.0 authentication flows from mobile apps. These include:
- Authentication requests should only be made through external user agents, such as the browser. This results in better security, and enables use of the user’s current authentication state, making single sign-on possible. Conversely, this means that authentication requests should never be made through a WebView. WebView controls are unsafe for third parties, as they leave the authorization grant and user’s credentials vulnerable to recording or malicious use. In addition, WebView controls don’t share authentication state, meaning single sign-on isn’t possible.
- Native apps must request user authorization by creating a URI with the appropriate grant types. The app then redirects the user to this request URI. A redirect URI that the native app can receive and parse must also be supplied.
- Native apps must use the Proof Key for Code Exchange (PKCE) protocol, to defend against apps on the same device potentially intercepting the authorization code.
- Native apps should use the authorization code grant flow with PKCE. Conversely, native apps shouldn’t use the implicit grant flow.
- Cross-Site Request Forgery (CSRF) attacks should be migitated by using the state parameter to link requests and responses.
Ultimately, the OAuth 2.0 authentication flow for native apps becomes:
- The native app opens a browser tab with the authorisation request.
- The authorisation endpoint receives the authorisation request, authenticates the user, and obtains authorisation.
- The authorisation server issues an authoration code to the redirect URI.
- The native app receives the authorisation code from the redirect URI.
- The native app presents the authorisation code at the token endpoint.
- The token endpoint validates the authorization code and issues the requested tokens.
I’d previously written a Xamarin.Forms
sample to do this without any additional libraries, and
one with the
IdentityModel.OidcClient2 library, which is OpenID certified. Both samples consume endpoints on a publically available
IdentityServer site, and use platform code to invoke native browsers.
Enter Xamarin.Essentials
Xamarin.Essentials recently introduced a
WebAuthenticator class, which is a web navigation API that can be used for authentication with web services. The class contains a single method,
AuthenticateAsync, which starts an authentication flow by navigating to a specified URI, and then waits for a callback/redirect to the redirect URI scheme. The whole point of the class is that it avoids having to write platform code to invoke native browsers. For more information, see the
WebAuthenticator doc.
I decided to test drive
WebAuthenticator by using it to initate an authentication flow with IdentityServer. However, an authentication flow from a mobile app should include a web backend between the mobile app and the authentication provider. This is to avoid including client secrets in your mobile app, which is considered to be insecure. However, I’ve avoided a web backend as middleware here, as I just want to see what’s involved in authenticating with IdentityServer using the
WebAuthenticator class.
However, before you can use
WebAuthenticator to authenticate with IdentityServer, you first have to perform some platform specific setup.
Platform setup
On Android, you require an
IntentFilter to handle your redirect URI. This can be accomplished by subclassing the Xamarin.Essentials
WebAuthenticatorCallbackActivity class:
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(new[] { Intent.ActionView},
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable},
DataScheme = "io.identitymodel.native",
DataHost = "callback")]
public class WebAuthenticationCallbackActivity : WebAuthenticatorCallbackActivity
{
}
You’ll also need to overide the
OnResume method in your
MainActivity class, to call back into Xamarin.Essentials:
protected override void OnResume()
{
base.OnResume();
Xamarin.Essentials.Platform.OnResume();
}
On iOS, you’ll need to add your app’s redirect URI to your Info.plist file:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>WebAuthenticatorDemo</string>
<key>CFBundleTypeRole</key>
<string>None</string>
<key>CFBundleURLSchemes</key>
<array>
<string>io.identitymodel.native</string>
</array>
</dict>
</array>
You’ll also need to overide the
OpenUrl method in your
AppDelegate class, to call back into Xamarin.Essentials:
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
if (Xamarin.Essentials.Platform.OpenUrl(app, url, options))
return true;
return base.OpenUrl(app, url, options);
}
On UWP, you need to declare your redirect URI in your Package.appxmanifest file:
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="WebAuthenticatorDemo.UWP.App">
...
<Extensions>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="io.identitymodel.native">
<uap:DisplayName>WebAuthenticatorDemo</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application>
</Applications>
Start an authentication flow with WebAuthenticator
An authentication flow with IdentityServer can be started by creating the start URI, and passing it to the
AuthenticateAsync method along with the redirect URI:
string url = identityService.CreateAuthorizationRequest();
var authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url), new Uri(Constants.RedirectUri));
If you aren’t familiar with the process of building an authorization request to IdentityServer, I recommend you look at the
IdentityService class in the
sample. It involves creating a long query string that contains data that indicates to IdentityServer what kind of authentication flow to kick off.
The
AuthenticateAsync method returns a
WebAuthenticatorResult object, which contains a number of properties, such as
AccessToken, and
RefreshToken. More importantly, it has a
Properties dictionary that contains all of the query string or URI fragment properties, that can then be accessed by their key. Therefore, the
Properties dictionary can be parsed to build the raw response sent by IdentityServer:
string raw = ParseAuthenticatorResult(authResult);
authorizeResponse = new AuthorizeResponse(raw);
string ParseAuthenticatorResult(WebAuthenticatorResult result)
{
string code = result?.Properties["code"];
string idToken = result?.IdToken;
string scope = result?.Properties["scope"];
string state = result?.Properties["state"];
string sessionState = result?.Properties["session_state"];
return $"{Constants.RedirectUri}#code={code}&id_token={idToken}&scope={scope}&state={state}&session_state={sessionState}";
}
The result of a successful authentication flow is that the
AuthorizeResponse object contains an authorization code, that can then be exchanged for the requested tokens:
UserToken userToken = await identityService.GetTokenAsync(authorizeResponse.Code);
If the
UserToken object contains an access token, it can be used when making API calls to IdentityServer:
var content = await identityService.GetAsync($"{Constants.ApiUri}test", userToken.AccessToken);
Wrapping up
Although there's a lot going on here, in terms of generating the start URI, and processing the response from the redirect URI, using the
WebAuthenticator class has avoided having to write platform code to invoke native browsers. This can be a considerable amount of work, depending on the platform, and it can also be confusing if you aren't familiar with the intricacies of each platform.
I've deliberately left out a lot of code from this blog post, which generates the queries to send to IdentityServer. This is because my aim wasn't to cover the intricacies of IdentityServer, but instead how to initiate an authentication flow with
WebAuthenticator, and how to process its response. I also said that all of this is simple, but it’s only simple if you have a good understanding of the OAuth 2.0 spec. If you don’t, it can be overwhelming. Therefore, in my next blog post I’ll look at simplifying all of this by using the
IdentityModel.OidcClient library.
The sample this code comes from can be found on
GitHub.