Common Gotchas with AppAuth
RFC 8252 - OAuth 2.0 for Native Apps (aka AppAuth pattern) describes how native applications should authorize with OAuth 2.0 and OpenID Connect Authorization Servers, that is, through external user-agents. You can still hear the Product Manager’s outcry over the use of browser app switches and webviews in favour of native UI components.
Why not native? Native UI component forms with username/password rely on the use of Resource Owner Password Credentials (ROPC) grant type. However, this grant type does not bring SSO and lacks in Refresh Token support as well as MFA and Step-up Authentication.
But it is a part of the OAuth 2.0 spec, we can use it safely… With 100% certainty this is
not true for native mobile applications. Here’s what the specification says:
The resource owner password credentials grant type is suitable in cases where the resource owner has a trust relationship with the client, such as the device operating system or a highly privileged application. The authorization server should take special care when enabling this grant type and only allow it when other flows are not viable.
There can be no trusted relationship with a distributed application in which client secrets are not really secrets. Today, Native Apps shouldn’t be trusted with neither client secrets, nor End-User Credentials.
AppAuth provides a secure alternative to ROPC authorization, without a significant impact on user experience. It uses an external user agent without a noticeable context switch, and handles many common pitfalls and mistakes.
There are great implementations of the AppAuth pattern for iOS and Android. However, they are not without issues. Here are a few to look out for.
There are three common pitfalls on iOS: Selection of the correct user agent, cookie sharing and cookie synchronisation.
1. The Right User-Agent for the Job
The following table shows the user-agent that AppAuth-iOS SDK will use, depending on iOS and the SDK’s versions automatically. If you need to implement features that require the access to an authenticated user within a webview it is important that you replicate this matrix. These features might include opening a profile page or using RP-Initiated Logout from Session Management 1.0.
|iOS \ AppAuth-iOS||< 0.90.0||>= 0.90.0|
|7-8||Mobile Safari||Mobile Safari|
- Mobile Safari - the only disadvantage is User Experience: the user must leave your app in order to authorize and then switch back. Furthermore, it is possible an app will not pass the AppStore review process.
- SFSafariViewController (SVC) - introduced in iOS 9, it shares cookies (and other Web Storage API data) with Mobile Safari. It provides a (UI|WK)WebView-like experience but unlike in those, the activity within is not visible to the host app and it features a Read-Only address bar. In iOS 11 Safari data sharing was stripped from SVC and each new instance will therefore have its own separate cookie jar.
- SFAuthenticationSession (SAS) - introduced in iOS 11, it provides the previous SVC behaviour but a new user consent dialog is shown before the view opens. This was intended to only show once per IdP, but currently still opens on every authorization, and indeed even for logout requests with the same message which cannot be customized. Looks pretty weird, especially for logout requests. Hopefully Apple will continue iterating over the solution to smooth it out.
Based upon the AppAuth compatibility table above, it is clear >= 0.90.0 version of AppAuth-iOS should be used in order to achieve SSO on all supported iOS major versions.
2. Cookie Sharing Restrictions
In order for Mobile Safari cookies to be consistently shared between SVC or SAS on different iOS major versions, they must not be session based. Instead, you have to set an expiration on your session cookies for them to be persistent and available in the host app’s view.
Set-Cookie:_session=854558f4-fe9a-455d-9d0f-4cbc9ef76035; path=/; expires=Sat, 23 Dec 2017 12:30:52 GMT; secure; httponly
3. Cookie sync takes time
There have been several reports from AppAuth users that on some iOS versions (10, 11) the shared cookie jar does not get synced between apps and Safari immediately. If you see odd behaviours around SSO you might try to test with a bit of a delay between app switches and view triggers to make sure you’re not experiencing this odd behaviour. Also be sure to report your issues.
Similarly to iOS, Android has two quirks to look out for and takes a while to get right.
1. Again, The Right User-Agent for the Job
Unlike with iOS, Android devices come with a variety of system browser and custom tab distributions, some of them work fine for AppAuth, some of them not so much. On top of that Android users are able to change the default system browser.
Sadly, AppAuth-Android does not keep tabs on the 100% working distributions. But it does give you
the option to whitelist or blacklist them, look for the
setBrowserMatcher API to customise the
browser matching for your app and tailor it to your target audience devices.
2. User Interactions are Required
When the End-User is already logged in and has authorized your app’s client in the past, it is very common the Authorization Server will not render any further prompts. Due to privacy reasons, Android engineers have decided to require some explicit user input, e.g. a button (or link) click within custom tabs in order to trigger RedirectUriReceiverActivity. If no such action happened within the authorization process RedirectUriReceiverActivity will not be called with the authorization response parameters.
A few solutions to this issue
- authorization request parameter
prompt=consentshould force the authorization server to re-render a given prompt screen even on subsequent authorizations. The Authorization Server needs to support this prompt and behaviour.
- server side client property
application_type=native. Since asking for a complete re-authorization is an overkill, some Authorization Servers allow you to set the client type (application_type) to
nativein order to force a screen on every native app authorization. This screen at the very least might show something along the lines “You’ve already authorized this application in the past” and a continue button.
- last resort solution would be to use a webpage as a the redirect url, this page would read the query string parameters and offer a link/button that the user would have to click in order to be transferred back to the host app.
Digging around this quirk I realized that an RP-Initiated Logout feature on one of my services does not render a prompt if id_token_hint is still fresh and yet the Activity is getting called. Turns out there’s a self-submitting form to an endpoint which finally does the post_logout_redirect_uri redirect being rendered first. It is this self-submitting form that makes the requirement pass. This is a bug and should not be relied upon.
Both iOS and Android
The current releases of AppAuth for iOS (0.92.0) and Android (0.7.0) do not perform signature validations of the ID Token. Don’t panic, that is actually alright for the AppAuth pattern since it relies purely on the authorization code flow and tokens only come from a TLS secured token endpoint of the Authorization Server. The libraries also lack OpenID Certification because they don’t perform some of the ID Token claim checks that a well-behaved client should. There are ongoing efforts that would surely welcome your contribution for iOS and Android.