Delegated authentication for Discourse Mobile app

Originally when we built the mobile app Apple offered seamless embedding of Safari in mobile apps.

All was good, you could log in to Safari, and magically with one click have the application authenticated.

However, fast forward a few years and Apple released iOS 11. Sadly, this broke “shared cookies”, this meant that you had to log in to the application web sites AGAIN after already being logged in, in Safari.

To add additional layers of pain :crying_cat_face: , cookie management in Safari View Controller is somewhat sketchy. In iOS 10 we used to see regular random logoffs in the app, this got a bit better in iOS 11, now iOS 12 is out we are seeing all sorts of edge cases and weirdness.

At Discourse we are committed to the web and the future of the web. The long game is that no app will be needed and the operating systems on the phone will provide all the features we want (notifications, dedicated PWA and so on). Luckily Android are pushing forward here, and the need for an App is lessened every year. Apple are making some slow starts here, but I estimate it will be at least 2-3 years prior to them catching up to where Android is today.

Delegated authentication

To solve our immediate pain of

  • Double login required
  • Safari View Controller random log offs
  • Safari View Controller trouble handling authentication redirects with SSO / OAuth
  • No way to login via email
  • No way to identify traffic as application traffic vs mobile safari traffic

We are going to introduce a system of “delegated authentication” using user API.

When asking for basic info about the site, the site will signal it supports the new interfaces to allow for a transition time.

Basic flow (not logged on to app):

  1. App opens to Safari on the Mobile phone asking for a token with notifications,session_info scope AND an extra param to ask for a one time password. .

  2. The UX on Discourse communicates it will allow those scopes and allow the mobile to login.

  3. Discourse generates a time limited, single use login token which is a SecureRandom.hex, this can be stored in redis. It is returned encrypted to the application with rest of the API key info.

  4. The app then open Discourse in Safari View Controller at a special new URL.

  5. That redirects back to / and user is logged on to SVC.

Advanced flow (user already has an api key but is not logged on to site)

  1. When opening SVC from the app we will always add ?user_api_public_key=USER_API_PUBLIC_KEY

  2. This allows us to flag the session as a mobile app session and ensure user is logged on.

  3. If the user is logged in and all is good an ember initializer will strip the ?user_api_public_key=USER_API_PUBLIC_KEY with replace state.

  4. If user is not logged on AND we know the public key is legit, we will redirect back to the app, asking it to get another OTP. discourse://otp=true&

  5. If app knows it needs another otp for the site it opens Safari and ask for an otp: ${site.url}/user-api-key/otp?public_key=[...]

  6. Discourse in Safari will display a simple page “Would you like to allow Discourse Mobile to access the site” or something like that

  7. When user clicks button it will do the same redirects the basic flow does.

Delegated auth is generally useful for all user api keys so this work is not throwaway. For example an Electron wrapper for Discourse could use the same mechanism to log in when you are already logged in to Chrome.

That said, our immediate pain is iOS mobile app.

Additionally part of this work is a review of our user api key system, and long needed documentation of the system, cause people occasionally ask about this and documentation is very weak.

This work is also strategic, cause it means that people will always be logged on to Discourse in Safari (every single app user) which will make transitioning to other systems easier. Maybe in a few years all the app will do is automate creation of PWA links for Apple. I don’t know, but this moves us in the right direction.


Does this also solve my problem?

Basically right now users of my mobile application, need to login to my forum when they open it with chrome custom-tab (for the first time). I think its a bad user experience, cause they have already login to app with same credentials of forum via api.

This sounds like a great improvement - getting set up with the app is a pretty frustrating process right now!

I had to read through this spec twice before I realised you weren’t talking about two-factor “one time passwords”. Is there a reason for this terminology? In core we already have the concept of “tokens” - these are used for the “login by email” system. I think it makes sense to build on this, rather than making a whole new redis-backed system.

As an aside, is there a reason why we have a discourse-specific process for “User API Keys”, rather than following some open specification like OAuth/OIDC?


Yeah we can use the existing token infrastructure here, however I want to make sure that

  1. We ship the token encrypted using the apps public key
  2. The token expires real quick

The reason I went for a new term is cause tokens are stored in the database and I feel this is overkill for something that is expiring this quickly and used this often.

But will leave @pmusaraj to interpret how to implement this.

The big reason both Discourse SSO and this system uses a custom protocol, is cause we control the protocols a tiny bit more and can make them simpler and more powerful.

For user api keys, I wanted the “private access token” to be encrypted using the client public key. I don’t think you can swing that requirement with OIDC or OAuth.

This was important cause I wanted to avoid key hijacking on phones which is something that has happened in the past:

  • Discourse App asks Safari for “payload”
  • Discourse redirects information back to discourse://payload=secret:abcdef
  • Some different app intercepts unencrypted payload and has access to the key
  • It redirects back to Discourse App

Compare to what we have today:

  • Discourse App asks Safari for “payload”, provides public key for app
  • Discourse redirects information back to discourse://encrypted_payload=ABC1231252ABC
  • Some different app intercepts payload and has nothing

All of this said, I am open to making an OIDC provider plugin for Discourse if we want more adoption, but at the moment so few seem to be asking for this.


I have started working on this and have a few things to verify regarding the advanced flow:

  • I don’t think we need to send the public key on every request in SafariViewController. The app currently sends discourse_app=1 which flags the session as coming from a mobile app. If user is logged in, discourse_app=1 can be stripped by Ember and the user can continue using the site normally.
  • If user is not logged in, Discourse in the SVC will redirect back to the app via the custom discourse:// URL scheme. Here too, I don’t think we need URL parameters, the request to the app can simply be discourse://request-one-time-password. The app, upon receiving this request, will know which site was used last and it will thus know which site url and public key to use in the subsequent OTP Safari request.
  • The next step (i.e. step 5 in advanced flow above) would be a simplified version of the initial Safari authentication request in the basic flow, but this time only passing the one-time-password scope, without notifications,session_info, those are already working for the site in the ReactNative context (and they’re not needed in SVC).
  • If user is already authenticated in Safari, then the simple confirmation page “Would you like to allow DiscourseHub access to the site” is shown. If not, the user has to login first. I’m not sure whether the original spec assumed this last bit or not @sam, but it sounds like the safest and most transparent flow for users (and if this advanced flow is only hit when SVC cookies go missing, users should almost always be already authenticated in Safari).
  • In both basic and advanced flows, I’m assuming we will use Apple’s ASWebAuthenticationSession in the Safari auth steps. This does introduce an iOS system alert (“Discourse app wants to use x site to Sign In”) in the flow, but it is the standard way to do this in iOS 12.
  • And finally, to address @david’s point, I agree with Sam, we should not use email tokens here. As Sam notes, these tokens expire very quickly, I would assume 5-10 mins max. And they will be a non-optional part of the user api key system, unlike email tokens which are disableable via the “enable local logins via email” site setting.

I think there is an aspect of security here. In fact to be “100% correct” we would sign a piece of transient data, like current time with the public key. But we can hold off on this for now if it is too complicated.

The trouble with only having discourse_app=1 is that this ends up being an “open redirect” of sorts.

  1. User installs app
  2. User logs in to meta
  3. Another browser without the cookies hits
  4. This will trigger “session re-establishment” which will redirect back to the app and then back to for auth despite SVC already having the right cookie.

This can be particularly confusing if is visited from a random browser on the phone, or even from the desktop.

By specifying the public key at least we have some way of tracking this and it is harder to exploit since these public keys are still bound to user and not exposed anywhere to the public.

Again this is a security thing. We don’t want random browsers to hit this link, forcing you to get a new OTP when you already have one. Signing with transient data is ideal here, but public key requirement at least means that we will not issue an OTP sequence from random apps.

If (1) validates a signature though and we echo back some piece of data then this would be safe against replays as well.

Yes this sounds right

I am not sure about this, I worry stuff will get complex with SSO that bouces you between sites and so on. I would prefer to go without it for now.


Yes, I see the redirect issue and the confusion it would introduce. Signing a piece of transient data makes sense, I will look into that.

Re: ASWebAuthenticationSession, I have already done some basic testing with it, the implementation is simple and it seems to work well, including with SSO. I don’t think SSO bouncing between sites would be any different than in the Safari app. It would be nice to at least test this. If there are any issues, switching back to launching the Safari app instead would be straightforward.


Re ASWebAuth the only difference is that you stay in the app vs get bounced to Safari? (And pay the price of an extra pop up for not being bounced)

Yes, you stay in the app, and you need to confirm the extra popup. Unlike in Safari, you don’t see an address bar, only a “Cancel” button in the top left corner.


As this feature is in the new release, can I ask how actually it works?

Please wait a few more weeks, while we test the new version of the app

1 Like

Now that the ios app is released, can we have some documentations about this feature?

The app isn’t released.

The beta began two days ago.


@hosna last week I updated User API keys specification, which now includes documentation for delegated authentication using a one-time-password. My reply to this question from about 10 days ago might also be useful.