Discourse. SSO in 10 steps (Docker based)

Ilya Zykin
6 min readJan 22, 2025

--

How to setup SSO for Discourse when you have a Ruby on Rails web project as a Login server

Step 1. Stable Branch for the Win!

Always use stable branch to start your experiments with Discourse.
https://github.com/discourse/discourse/tree/stable

Main branch is unstable and it is normal when it does not work.

Step 2. Installation

My docker-compose.yml may help

name: discourse

services:
postgres:
image: postgres:13.4
environment:
- POSTGRES_USER=postgres
- POSTGRES_DB=discourse_development
- POSTGRES_PASSWORD=qwerty
- PSQL_HISTFILE=/root/log/.psql_history
ports:
- ${POSTGRES_PORT-5432}:${POSTGRES_PORT:-5432}
volumes:
- ./data/PSQL:/var/lib/postgresql/data

redis:
image: redis:7.4.2
volumes:
- ./data/REDIS:/data

app:
build:
context: ..
dockerfile: ./docker/Dockerfile
environment:
- DISCOURSE_DB_HOST=postgres
- DISCOURSE_DB_PORT=5432
- DISCOURSE_DEV_DB=discourse_development
- DISCOURSE_DB_USERNAME=postgres
- DISCOURSE_DB_PASSWORD=qwerty

- DISCOURSE_REDIS_HOST=redis
- DISCOURSE_REDIS_PORT=6379
ports:
- "3000:3000"
- "4200:4200"
volumes:
- ..:/app
command: sleep infinity

My Dockerfile (Mac OS / M1)

# https://meta.discourse.org/t/install-discourse-on-ubuntu-or-debian-for-development/14727/1
# bash <(wget -qO- https://raw.githubusercontent.com/discourse/install-rails/main/linux)
# https://github.com/discourse/discourse/blob/main/docs/DEVELOPER-ADVANCED.md

FROM ruby:3.2-bookworm

ARG PROJECT_PATH=${PROJECT_PATH:-__PROJECTS__/discourse}

# INSTALL DEPENDENCIES
#
# build-essential - Essential build tools for compiling software
# libxslt1-dev - Library for XSLT processing
# libcurl4-openssl-dev - Library for HTTP requests with SSL support
# libksba8 - Libraries for cryptographic functionality
# libksba-dev - Libraries for cryptographic functionality
# libreadline-dev - Library for GNU readline (interactive command line)
# libssl-dev - SSL/TLS support library
# zlib1g-dev - Compression library
# libsnappy-dev - Snappy compression library
# libyaml-dev - YAML parsing library
# libpq-dev - PostgreSQL client library for database connections
# postgresql-client - PostgreSQL client for interacting with remote PostgreSQL databases
# telnet - Tool for testing network services
# nano - Command-line text editor
# git - Git version control system
# net-tools - Utilities for diagnosing and testing network connections
# dnsutils - Tools for DNS queries
# iputils-ping - Utilities for pinging hosts and testing connectivity
# curl - Curl command-line tool for transferring data with URLs
# wget - Wget command-line tool for downloading files
# tzdata - Timezone data for managing time zones
# advancecomp - Tools for optimizing PNG and MNG files
# jhead - Tool for displaying and manipulating Exif data
# jpegoptim - Tool for optimizing JPEG files
# libjpeg-turbo-progs - Tools for optimizing JPEG files
# optipng - Tool for optimizing PNG files
# pngcrush - Tool for optimizing PNG files
# pngquant - Tool for optimizing PNG files
# gnupg2 - GNU Privacy Guard for secure communication and data storage
# libjpeg-dev - Development files for the JPEG library
# libpng-dev - Development files for the PNG library
# libtiff-dev - Development files for the TIFF library
# libwebp-dev - Development files for the WebP library
# libxml2-dev - Development files for the XML library
# libltdl-dev - Development files for the libtool library
# libfreetype6-dev - Development files for the FreeType library
# liblcms2-dev - Development files for the Little CMS library
# liblqr-1-0-dev - Development files for the Liquid Rescale library
# libfftw3-dev - Development files for the Fastest Fourier Transform in the West library
# ghostscript - Interpreter for the PostScript language and PDF
#
RUN apt-get update -qq && apt-get install -y \
build-essential \
libxslt1-dev \
libcurl4-openssl-dev \
libksba8 \
libksba-dev \
libreadline-dev \
libssl-dev \
zlib1g-dev \
libsnappy-dev \
libyaml-dev \
libpq-dev \
postgresql-client \
telnet \
nano \
git \
net-tools \
dnsutils \
iputils-ping \
curl \
wget \
tzdata \
advancecomp \
jhead \
jpegoptim \
libjpeg-turbo-progs \
optipng \
pngcrush \
pngquant \
gnupg2 \
libjpeg-dev \
libpng-dev \
libtiff-dev \
libwebp-dev \
libxml2-dev \
libltdl-dev \
libfreetype6-dev \
liblcms2-dev \
liblqr-1-0-dev \
libfftw3-dev \
ghostscript \
&& rm -rf /var/lib/apt/lists/*

# WORKING DIRECTORY
#
WORKDIR /tmp

# Download and install ImageMagick 7
RUN cd /tmp && wget https://imagemagick.org/download/ImageMagick.tar.gz && \
tar xvzf ImageMagick.tar.gz && \
cd ImageMagick-* && \
./configure && \
make && \
make install && \
ldconfig /usr/local/lib && \
cd .. && \
rm -rf ImageMagick* && \
magick --version

# OXIPNG - OPTIMIZE PNG FILES (ARM64)
# https://github.com/shssoichiro/oxipng/releases/
RUN wget https://github.com/shssoichiro/oxipng/releases/download/v9.1.3/oxipng_9.1.3-1_arm64.deb
RUN dpkg -i oxipng_9.1.3-1_arm64.deb

SHELL ["/bin/bash", "-c"]

# INSTALL NODEJS AND PNPM AND YARN
#
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
RUN source /root/.nvm/nvm.sh && nvm install lts/hydrogen --default

# RUN source /root/.nvm/nvm.sh && \
# corepack enable pnpm && \
# corepack use pnpm@9

RUN source /root/.nvm/nvm.sh && \
npm install -g yarn

# WORKING DIRECTORY
#
WORKDIR /app

# INSTALL RUBY ON RAILS DEPENDENCIES
#
RUN gem install bundler -v 2.5.9

COPY ${PROJECT_PATH}/Gemfile /app/Gemfile
COPY ${PROJECT_PATH}/Gemfile.lock /app/Gemfile.lock

RUN bundle install --frozen

# INSTALL NODEJS DEPENDENCIES
#
RUN source /root/.nvm/nvm.sh && npm install -g svgo

COPY ${PROJECT_PATH}/package.json /app/package.json
COPY ${PROJECT_PATH}/patches /app/patches

RUN source /root/.nvm/nvm.sh && yarn -v

Step 4. DB setup

export SKIP_MULTISITE=1
congif/database.yml

development:
adapter: postgresql
database: discourse_development
username: postgres
password: password
host: postgres
port: 5432
pool: 5

test:
adapter: postgresql
database: discourse_test
username: postgres
password: password
host: postgres
port: 5432
pool: 5
$ yarn install
$ SKIP_MULTISITE=1 bundle exec rake db:create db:migrate

Step 5. Create Admin

In the container on localhost

$ bin/rails admin:create

Email: test@newforum.com
Password:
Repeat password:

Ensuring account is active!

Account created successfully with username `newforum`
Do you want to grant Admin privileges to this account? (Y/n) Y

Your account now has Admin privileges!

Step 6. Launch the project

export DISCOURSE_HOSTNAME=localhost
export DISCOURSE_PORT=4200
export SKIP_MULTISITE=1

bin/ember-cli -u

http://localhost:4200

Step 7. DiscourseConnect (SSO) Settings

http://localhost:4200/admin/site_settings/category/login

enable_discourse_connect : must be enabled, global switch
discourse_connect_url: the offsite URL users will be sent to when attempting to log on
discourse_connect_secret: a secret string used to hash SSO payloads. Ensures payloads are authentic.

My Login server is launched on localhost:3002

enable discourse connect: true
discourse connect allowed redirect domains: http://localhost:3002
discourse connect url: http://localhost:3002/connect/sso
discourse connect secret: MySecretKey

Step 8. Login Button Now leads to http://localhost:3002/connect/sso

http://localhost:3002/connect/sso?sso=PAYLOAD&sig=SIG

class SsoController < ActionController::Base
def sso
render json: params
end
end
Rails.application.routes.draw do
get '/connect/sso', to: 'sso#sso', as: 'sso'
end

Let’s learn a request to Loging server

Step 9. SSO controller on your side

Let’s implement SSO controller

require 'base64'
require 'openssl'

class SsoController < ActionController::Base
# Must be equal to what you saved in Discourse settings
DISCOURSE_SECRET = "MySecretKey"

before_action :store_user_location!, if: :storable_location?
before_action :authenticate_user!

def sso
# Extract parameters from the request
sso = params[:sso]
sig = params[:sig]

# Validate the signature
unless valid_signature?(sso, sig)
render plain: 'Invalid signature', status: :unauthorized
return
end

# Decode the SSO payload
sso_params = decode_sso_payload(sso)

# Build a payload to send back to Discourse
response_payload = {
nonce: sso_params['nonce'],
email: current_user.email,
external_id: current_user.id.to_s,
username: current_user.username
}

# Generate response payload
encoded_response, response_sig = generate_response(sso_params, response_payload)

# http://postgres:4200/session/sso_login?sso=PAYLOAD=&sig=SINGNATURE
# Redirect back to Discourse
# MAY NEED FOR ADDITIONAL SETTINGS
redirect_to "#{sso_params['return_sso_url']}?sso=#{encoded_response}&sig=#{response_sig}"
end

private

# Validates the signature using the Discourse secret key
def valid_signature?(sso, sig)
computed_sig = OpenSSL::HMAC.hexdigest('sha256', DISCOURSE_SECRET, sso)
computed_sig == sig
end

# Decodes the Base64-encoded SSO payload
def decode_sso_payload(sso)
decoded_sso = Base64.decode64(sso)
Rack::Utils.parse_nested_query(decoded_sso)
end

# Generates the response payload and its signature
def generate_response(sso_params, response_payload)
raw_response = URI.encode_www_form(response_payload)
encoded_response = Base64.strict_encode64(raw_response)
response_sig = OpenSSL::HMAC.hexdigest('sha256', DISCOURSE_SECRET, encoded_response)

[encoded_response, response_sig]
end

def storable_location?
request.get? && is_navigational_format? && !devise_controller? && !request.xhr?
end

def store_user_location!
session[:return_to] = request.original_url
end
end

SiteSetting.port = 4200
SiteSetting.force_hostname = “localhost”
Discourse.base_url => “
http://localhost:4200"

Step 10. Login and see your new Account

Conclusion

Now you know how to setup SSO for Discourse if your Login server it a Rails based application.

Happy coding!
Follow me on github: https://github.com/the-teacher

--

--

Ilya Zykin
Ilya Zykin

Written by Ilya Zykin

IT coach. React and Rails enthusiast. Passionate programmer, caring husband and pancake baker on Sundays. School teacher of computer studies in the past.

No responses yet