Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Discourse Sso Laravel Package

spinen/laravel-discourse-sso

Provides single sign-on between a Laravel application and a Discourse forum using Discourse's SSO protocol. It builds signed payloads, validates nonces, and lets users log in to Discourse with their existing Laravel credentials, keeping account data in sync and avoiding separate forum passwords.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package and pull in the config:
    composer require spinen/laravel-discourse-sso
    php artisan vendor:publish --tag=config   # inspect src/ if the tag differs
    
  2. Read config/discourse-sso.php first. It is the contract between your app and Discourse. You are looking for the shared secret (or the env key it reads) and any route/URL settings. Put the same value in .env as DISCOURSE_SSO_SECRET and confirm config:cache isn't serving a stale copy.
  3. Wire up Discourse. In the forum's admin panel under Settings → Login:
    • enable_sso = on
    • sso_url = the URL of the package's login route (e.g. https://app.example.com/discourse/sso)
    • sso_secret = the exact same secret string as your .env
  4. Check the route registration. Run php artisan route:list | grep -i sso. Many Spinen packages auto-register routes in the service provider; if not, define them in routes/web.php pointing at the package controller.
  5. First real use case: protect the SSO login route with auth (or your guard middleware), then click Log In on the Discourse forum. Discourse will redirect to your app; after Laravel auth succeeds, your app redirects back and Discourse creates/links the account. That round trip is the whole feature — get it working end-to-end before customizing payload fields.

Where to look next: src/ for the payload signing/validation class and the controller handling the two legs of the flow. That class is the extension point you will actually touch.

Implementation Patterns

The two-leg flow. Discourse SSO always has two hops:

  • Inbound (login): Discourse sends ?sso=<payload>&sig=<hmac> to your app. Validate the sig against the secret, base64-decode the payload, and read the nonce.
  • Outbound (callback/return): Your app builds a payload for the authenticated user, includes the same nonce, signs it, and redirects to {sso_url}?sso=<payload>&sig=<hmac>.

Most day-to-day integration work happens on the outbound leg, where the user record is mapped.

Mapping your user model to the payload. The fields Discourse cares about:

$payload = [
    'nonce'       => $nonce,                 // echo it back — required
    'external_id' => $user->getKey(),        // must be stable forever
    'email'       => $user->email,
    'username'    => $user->username,        // or derive from email/name
    'name'        => $user->name,
    'avatar_url'  => $user->avatar_url ?: null,
    'admin'       => $user->hasRole('admin'),
    'moderator'   => $user->hasRole('moderator'),
    // custom fields are prefixed and merged in
    'custom'      => ['plan' => $user->plan],
];

Prefer your own controller over the package's. If the bundled controller hardcodes Auth::user(), subclass it or write a thin controller that calls the package's signing helper. That keeps guard/tenant logic in your hands while still using the package's HMAC implementation (so you don't re-implement base64url + signature quirks).

class SsoController extends Controller
{
    public function __construct(private DiscourseSso $sso) {}

    public function login(Request $request)
    {
        $data = $this->sso->validate($request->query('sso'), $request->query('sig'));
        return redirect()->route('discourse.sso.callback')
            ->with('sso_nonce', $data['nonce']);
    }

    public function callback(Request $request)
    {
        $user = $request->user();
        return redirect()->away($this->sso->buildRedirect($user, session('sso_nonce')));
    }
}

Authorization patterns that pay off:

  • Middleware ['auth', 'verified'] on the login leg so unverified accounts never reach Discourse.
  • Role-based admin/moderator flags computed from your policy/roles, not hardcoded.
  • Multi-tenant: resolve the tenant from the host/subdomain before building the payload.

Syncing back. The login leg is the natural place to record discourse_last_login_at, provision a Discourse-specific profile row, or reconcile a username collision before the payload is sent.

Testing. You rarely want a live Discourse in CI. Generate a payload with the known secret and assert your controller redirects with a correctly signed sso param. A unit test on the encode/decode helpers plus one HTTP feature test on the redirect covers most regressions.

Gotchas and Tips

Signature and encoding

  • Discourse uses URL-safe base64 without padding (base64url, -/_, no =). Plain PHP base64_encode will produce a signature mismatch. Use rtrim(strtr(base64_encode($s), '+/', '-_'), '=').
  • Sign the raw payload bytes, not the URL-encoded string. Decode query params with urldecode/$request->query() before HMACing.
  • The secret must match exactly — a trailing newline or accidental quotes in .env will cause an opaque 422 from Discourse. config:cache after setting it.

Payload rules

  • nonce must be echoed back or Discourse rejects the login.
  • external_id must be immutable. Switching from id to a UUID later orphans every Discourse account. Pick it deliberately on day one.
  • email must be the one Discourse should trust; don't send a stale or placeholder address or you'll get accounts Discourse can't verify.
  • Send the payload as a query string via http_build_query, not as a POST body — Discourse expects the redirect.
  • If you add custom fields, they go under custom.* and must be whitelisted in the Discourse SSO settings on the forum side, or they're silently dropped.

Package / framework compatibility

  • Last release was 2020-06-19. On Laravel 9+ verify: ServiceProvider::$namespace removal, package auto-discovery (extra.laravel.providers in composer.json), typed method signatures, and Route::middleware API changes. Expect to fork or add a dont-discover entry and register manually.
  • If routes are auto-registered, they may collide with your own web middleware group or CSRF expectations. The SSO endpoint is a GET so CSRF isn't an issue, but session middleware is — the inbound leg must see the authenticated session.
  • Middleware order matters: auth must run before the SSO controller resolves the user, or you'll redirect to Discourse with a null user.

Debugging

  • Log the inbound sso/sig, the decoded payload, and the outgoing payload before the redirect. Most failures look identical from the browser (Discourse shows "Login Error").
  • Distinguish signature failures from missing-user failures in logs — they manifest the same to the end user.
  • Test behind HTTPS + load balancer: set APP_URL correctly and configure TrustProxies, or the sso_url you redirect to may be http:// and Discourse rejects it.

Extension points & limits

  • Publish the config and, if supported, the controller/views so you can override without editing vendor files.
  • There is no back-channel logout: logging out of Laravel does not log the user out of Discourse, and vice versa. Build a redirect-based logout flow if you need synchronized sessions.
  • Any user who reaches the callback is effectively provisioned in Discourse — guard the login route tightly and decide explicitly what happens for soft-deleted or suspended Laravel users.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource