Skip to content

Instantly share code, notes, and snippets.

@j127
Last active October 28, 2019 15:10
Show Gist options
  • Save j127/e30efaad84a4c5137c3c6bed500b5dd3 to your computer and use it in GitHub Desktop.
Save j127/e30efaad84a4c5137c3c6bed500b5dd3 to your computer and use it in GitHub Desktop.
Rails 5.2 with Discourse SSO Authentication
# This is just a sketch. Please leave a comment if you have suggestions.
class AuthController < ApplicationController
# Generate and SSO URL and redirect the user to Discourse
def authenticate
# Save the ?destination=some_url parameter if it exists
destination = request.query_parameters['destination'] || root_url
session[:destination] = destination
# `nonce` here will have `:value` and `:created_at` keys
nonce = generate_nonce
session[:nonce] = nonce
discourse_auth_url = generate_auth_url(nonce[:value])
redirect_to discourse_auth_url
end
# Verify that Discourse approved the user's log-in credentials
def validate
sig = request.query_parameters['sig']
sso = request.query_parameters['sso']
nonce = session[:nonce] || nil
secret = Rails.application.credentials.discourse[:sso_secret]
decoded_sso = CGI.unescape(sso)
sso_hmac_sha256 = OpenSSL::HMAC.hexdigest('SHA256', secret, decoded_sso)
# Make sure the above two values are equal.
if sig == sso_hmac_sha256 && nonce_timestamp_is_valid(nonce)
user_data_querystring = Base64.decode64(sso)
user_data = Rack::Utils.parse_nested_query(user_data_querystring)
if nonce['value'] == user_data['nonce']
flash[:notice] = 'You are now logged in!'
else
puts "nonces didn't match"
flash[:notice] = 'Something went wrong. If you believe that this is a bug, please contact us.'
end
else
flash[:notice] = 'Something went wrong. If you believe that this is a bug, please contact us'
end
# Remove the nonce in any case
session.delete(:nonce)
redirect_to session['destination']
end
def error
end
def logout
discourse_root_url = Rails.application.credentials.discourse[:root_url]
discourse_api_key = Rails.application.credentials.discourse[:api_key]
client = DiscourseApi::Client.new(discourse_root_url)
client.api_key = discourse_api_key
client.api_username = 'webmasterbot'
# TODO: change `2` to the user's Discourse ID (which needs to be saved somewhere)
client.log_out(2)
flash[:notice] = 'You have been logged out.'
end
private
# Generate an authorization URL.
def generate_auth_url(nonce)
# These are set in the encrypted credentials file (see the README -- it's a Rails 5.2 feature)
secret = Rails.application.credentials.discourse[:sso_secret]
return_url = Rails.application.credentials.discourse[:return_url]
discourse_root_url = Rails.application.credentials.discourse[:root_url]
payload = "nonce=#{nonce}&return_sso_url=#{return_url}"
base64_payload = Base64.encode64(payload)
url_encoded_payload = CGI.escape(base64_payload)
hex_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, base64_payload).downcase
query = "sso=#{url_encoded_payload}&sig=#{hex_signature}"
destination_url = "#{discourse_root_url}/session/sso_provider?#{query}"
destination_url
end
# Generate a random nonce and save it temporarily. The nonce needs
# to be persisted for a short time.
def generate_nonce
{
value: SecureRandom.base64(16),
created_at: Time.now.to_i
}
end
# Ensure that nonce is valid for a limited time (3600 seconds is 60 minutes)
def nonce_timestamp_is_valid(nonce)
# TODO: why does this nonce key need to be accessed as a string
# instead of a symbol like everywhere else?
Time.now.to_i - nonce['created_at'] <= 3600
end
end
@j127
Copy link
Author

j127 commented Jan 24, 2019

And some JavaScript for log out:

const logoutButton = document.getElementById('logoutButton');
const currentPage = window.location.href;

logoutButton.addEventListener('click', e => {
    e.preventDefault();
    Rails.ajax({
        url: '/auth/logout',
        type: 'POST',
        success: () => {
            // reload so that it shows the flash message from the backend
            window.location.href = currentPage;
        }
    });
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment