Triggering account creation/login on external service when a user logs in on discourse

I have a JS plugin on my discourse site. This JS uses pocketbase to store data. Pocketbase is a “serverless” sqlite service that permits me to store files and JSON blobs. Pocketbase has a terrific JWT-based auth system so that once a user has an auth token, then can securely store data directly into pocketbase (without proxying through a backend server, etc.) directly from JS on the client side.

I am trying to figure out a way where I can generate a login on the pocketbase side automatically when a user logs into discourse.

My first attempt was to have the JS plugin make a call to a path on the server and include discourse auth cookies. Then, for that path, let nginx proxy it to a service (“login proxy”) which can decode the auth cookies and determine who the user is. With verified user information, the login proxy can then make a special call to pocketbase and get back a pocketbase auth cookie, and then send back that pocketbase auth cookie which the client side JS can use to make subsequent requests directly to pocketbase.

But, I’m having trouble decoding the discourse auth cookies (I imagine _t is the right cookie, but I don’t see a simple way to get at the user details and am worried that structure might change anyway).

Is there a smarter way to securely get at the email address of a logged in user? I don’t think this should happen on the client side, and would prefer to do this on the server side for obvious security reasons.

I don’t know enough of your stack or use case, but I think I’ve solved a similar problem before, and some ideas may be useful for you.

I have a Next.js app where I need the client-side to have a valid JWT to make calls to my backend API if there’s a Discourse session.

For this I use Discourse as my identity provider through DiscourseConnect.

In my case, I’m doing this with a single client-side fetch call with { credentials: "include" }, which only works because I have everything setup with a single domain and the fetch call transparently follows redirects.

My client fetches a custom /auth/token, which checks for the existence of _t (just to avoid a pointless redirect otherwise) and returns a redirect to a secured /session/sso_provider URL built following the docs in the linked topic, with nonce/sso/sig, and a return_sso_url pointing to a custom /auth/callback, which will extract the data sent by Discourse, build and return a JWT token my client can use from this moment on.

I believe your use case may be solved in a similar way.

Awesome, I’ll give this a try. Thanks so much.

1 Like

@renato A few questions if you don’t mind.

Does this mean I need to delegate the entirety of user management auth to the connect app? I am not sure if I want to do this, if so.

If connect is just an additional layer on top of the existing discourse user auth management, then that seems workable.

But, when I started reading about discourse connect, I am worried I now need to build and maintain an entirely new app for managing user auth, and I don’t really know how to scope this right now.

My answer assumes you are using Discourse as your identity provider (with its login/signup UIs), and wants to keep it that way.

On the Discourse side, enabling it is as simple as

However, you mentioned you’re building a plugin.

If you build “a path on the server” in a new controller action in a Discourse plugin, you can get the user from the session, call 3rd parties and return the JWT to your client.

Thanks so much for the discussion.

I read the link: Use Discourse as an identity provider (SSO, DiscourseConnect) - #8 by reverend_paco

But, I think this is if I want to send my users to a secondary site and manage auth there.

In my case, I have a piece of JS that runs on the discourse site. And, I want to have that JS call into a path on the same server and get a cookie back for pocketbase.

I actually use an nginx proxy in front of discourse, and so I just added a special route /pb/auth (for example). When my JS hits that route, then a backend proxy server (that is not inside discourse) accepts that connection, and tries to decode the _t session cookie.

I was doing it this way because it seems a little easier than adding a discourse plugin (I have less familiarity with that and the dev setup, etc.). If it is a simple matter of decoding a cookie using base64 and sha hashing, I thought that would give me a secured payload for telling me who the user is.

But, if you think there is a straightforward way to build a plugin that adds this route to discourse, I’m very interested in trying that. It seems like the right way long term. But, I’m an old Perl programmer, so I prefer the lazy route, and my nginx route seemed lazier. :slight_smile:

Quite the opposite: if you have a separate “site” (PocketBase in this example) and want Discourse to be the source of truth for user/auth management – like my Next.js example.

I’d start by reading

Great, I’m excited to read that. I started looking at the sample skeleton plugin (GitHub - discourse/discourse-plugin-skeleton: Template for Discourse plugins) and was a bit underwhelmed because it has no documentation at all.

At first glance I need to ask: is this tutorial adding code to the base rails installation for discourse? I’m OK with doing that if that’s the official way, but it feels like this is dangerous and would be better handled as a plugin (which can easily be uninstalled, disabled). Also, don’t I have to worry this will break upgrades to discourse if my code is not in the github repo?

For example here:

Does this mean I really jump into the container (./launcher enter app) and then edit /var/www/app/controllers/snack_controller.rb?

And, I actually just followed those instructions. I cannot get the /admin/snack.json route to work, even after running ./launcher rebuild app.

This tutorial looks to be about eight years old. Is this really the correct way to do things?

There are other guides, the date at the top is the topic creation but anything in Documentation should be up-to-date – let us know if you find any problems.

You can check the code of the existing Plugin for reference.

Nope:

I have the impression this hasn’t been updated yet

I think the file is now discourse/app/assets/javascripts/admin/addon/routes/admin-route-map.js at main · discourse/discourse · GitHub
and it was renamed in 2020.

2 Likes

Ok, I tried to follow the instructions. I tried to use this command rake plugin:create[pocketbase-auth] inside the container (since rake would not be available outside, right). But, it fails hard because I don’t have git setup inside the container.

As I read further, it appears I need to specify the git repo of the plugin to display a plugin in the admin section. But, I have not gotten to the point where I have even a barely working version of the plugin and I don’t have things inside a git repository.

EDIT: I didn’t read carefully, and indeed this requires a dev setup, which is clearly stated up front. I’ll work on that and come back to this.

I suspect that these struggles are because typically plugins are developed against a “dev” setup of discourse, not inside the docker container which I am running. That’s fine, but I wish the plugin developer guides started with that assumption and instructions on how to run that way. The recommended way to run discourse is using docker (which I love) but I think there is a gap between how to run things inside docker and do development inside the docs.


# rake plugin:create[pocketbase-auth]
Cloning 'https://github.com/discourse/discourse-plugin-skeleton' to '/var/www/discourse/plugins/pocketbase-auth'...
Initializing git repository...
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint: 
hint: 	git config --global init.defaultBranch <name>
hint: 
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint: 
hint: 	git branch -m <name>
Initialized empty Git repository in /var/www/discourse/plugins/pocketbase-auth/.git/
Author identity unknown

*** Please tell me who you are.

Run

  git config --global user.email "you@example.com"
  git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.

fatal: unable to auto-detect email address (got 'discourse@community-public-do-vm-app.(none)')
rake aborted!
Command failed with exit 128: git
/var/www/discourse/lib/tasks/plugin.rake:356:in `system'
/var/www/discourse/lib/tasks/plugin.rake:356:in `block (2 levels) in <main>'
/var/www/discourse/lib/tasks/plugin.rake:346:in `chdir'
/var/www/discourse/lib/tasks/plugin.rake:346:in `block in <main>'
/usr/local/bin/bundle:25:in `load'
/usr/local/bin/bundle:25:in `<main>'
Tasks: TOP => plugin:create
(See full trace by running task with --trace)

An update: I took your advice and developed a plugin. It works great and does exactly what I needed. Thanks for your help.

1 Like