- 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.
- 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.
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.
No comments:
Post a Comment