Advertisement
riking

Untitled

Apr 1st, 2014
478
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. {"post_id":41969,"version":7,"revisions_count":7,"username":"sam","display_username":"sam","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","created_at":"2014-04-01T22:12:47.585-04:00","edit_reason":null,"body_changes":{"inline":"<div class=\"inline-diff\"><p>Discourse now ships with official hooks to perform auth offsite. </p><h3>The Problem</h3><p>Many sites wish to integrate with a Discourse site, however want to keep all user registration in a separate site. In such a setup all Login operations should be outsourced to a different site. </p><h3>What if I would like SSO in conjunction with existing auth?</h3><p>The intention around SSO is to replace Discourse authentication, if you would like to add a new provider see existing plugins such as: <a href=\"https://meta.discourse.org/t/vk-com-login-vkontakte/12987\">https://meta.discourse.org/t/vk-com-login-vkontakte/12987</a></p><h3>Enabling SSO</h3><p>To enable single sign on you have 3 settings you need to fill out:</p><p class=\"diff-del\"></p><div class=\"diff-del lightbox-wrapper\"><a href=\"//meta-discourse.r.worldssl.net/uploads/default/3415/c68d5890b0f7bd4f.png\" class=\"diff-del lightbox\" title=\"Pasted image\"><img src=\"//meta-discourse.r.worldssl.net/uploads/default/_optimized/07c/3bf/3fa1d69ceb_690x207.png\" width=\"690\" height=\"207\" class=\"diff-del\"><div class=\"diff-del meta\"><del>\n</del><span class=\"diff-del filename\"><del>Pasted </del><del>image</del></span class=\"diff-del\"><span class=\"diff-del informations\"><del>798x240 </del><del>16</del><del>.</del><del>8 </del><del>KB</del></span class=\"diff-del\"><span class=\"diff-del expand\"></span class=\"diff-del\"><del>\n</del></div class=\"diff-del\"></a class=\"diff-del\"></div class=\"diff-del\"><p class=\"diff-ins\"><img src=\"/uploads/default/3415/c68d5890b0f7bd4f.png\" width=\"690\" height=\"207\" class=\"diff-ins\"></p class=\"diff-ins\"><p><code>enable_sso</code> : must be enabled, global switch<br><code>sso_url</code>: the <strong>offsite</strong> URL users will be sent to when attempting to log on<br><code>sso_secret</code>: a secret string used to hash SSO payloads. Ensures payloads are authentic.</p><p>Once <code>enable_sso</code> is set to true:</p><ul>\n<li>Clicking on login or avatar will, redirect you to <code>/session/sso</code> which in turn will redirect users to <code>sso_url</code> with a signed payload.</li>\n<li>Users will not be allowed to \"change password\". That field is removed from the user profile.</li>\n<li>Users will no longer be able to use Discourse auth (username/password, google, etc)</li>\n</ul><h3>What if you check it by mistake?</h3><p>If you check <code>enable_sso</code> by mistake and need to revert to the original state and no longer have access to the admin panel</p><p>run:</p><p></p><pre><code class=\"lang-auto\">RAILS_ENV=production bin/rails c\nirb &gt; SiteSetting.enable_sso = false</code></pre><h3>Implementing SSO on your site</h3><p>Discourse will redirect clients to <code>sso_url</code> with a signed payload: (say sso_url is <code>https://somesite.com/sso</code>)</p><p>You will receive incoming traffic with the following</p><p><code>https://somesite.com/sso?sso=PAYLOAD&amp;sig=SIG</code></p><p>The payload is a Base64 encoded string comprising of a <a href=\"http://en.wikipedia.org/wiki/Nonce\">nonce</a>. The payload is always a valid querystring. </p><p>For example, if the nonce is ABCD. raw_payload will be:</p><p><code>nonce=ABCD</code>, this raw payload is <a href=\"http://en.wikipedia.org/wiki/Base64\">base 64</a> encoded.</p><p>The endpoint being called must</p><ol>\n<li>Validate the signature, ensure that <ins>HMAC</ins><ins>-</ins><ins>SHA256 </ins><del>HMAC </del><del>256 </del>of sso_secret,<ins> </ins>PAYLOAD is equal to the sig</li>\n<li>Perform whatever authentication it has to</li>\n<li>Create a new payload with <strong>nonce</strong>, <strong>email</strong>, <strong>external_id</strong> and optionally (username, name, return_url) </li>\n<li>Base64 encode the payload</li>\n<li>Calculate a <ins>HMAC</ins><ins>-</ins><ins>SHA256 </ins><del>HMAC </del><del>256 </del>hash of the using sso_secret as the key and Base64 encoded payload as text </li>\n<li>Redirect back to <code>http://discourse_site/session/sso_login?sso=payload&sig=sig</code>\n</li>\n</ol><p>Discourse will validate that the nonce is valid (if valid it will expire it right away so it can no longer be used) it will attempt to:</p><ol>\n<li>Log the user on by looking up an already associated <strong>external_id</strong> in the SingleSignOnRecord model</li>\n<li>Log the user on by using the email provided (updating external_id)</li>\n<li>Create a new account for the user providing (email, username, name) updating external_id</li>\n</ol><h3>Security concerns</h3><p>The nonce (one time token) will expire automatically after 10 minutes. This means that as soon as the user is redirected to your site they have 10 minutes to log in / create a new account. </p><p>The protocol is safe against replay attacks as nonce may only be used once.</p><h3>Reference implementation</h3><p>Discourse contains a reference implementation of the SSO class:</p><p><aside class=\"onebox githubblob\"><header class=\"source\"><a href=\"https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb\">\n      github.com\n    </a>\n  </header><article class=\"onebox-body\"><h4><a href=\"https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb\" target=\"_blank\">https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb</a></h4>\n<pre><code class=\"\">class SingleSignOn\n  ACCESSORS = [:nonce, :name, :username, :email, :about_me, :external_email, :external_username, :external_name, :external_id]\n  FIXNUMS = []\n  NONCE_EXPIRY_TIME = 10.minutes\n\n  attr_accessor(*ACCESSORS)\n  attr_accessor :sso_secret, :sso_url\n\n  def self.sso_secret\n    raise RuntimeError, \"sso_secret not implemented on class, be sure to set it on instance\"\n  end\n\n  def self.sso_url\n    raise RuntimeError, \"sso_url not implemented on class, be sure to set it on instance\"\n  end\n\n  def sso_secret\n    @sso_secret || self.class.sso_secret\n  end\n\n  def sso_url\n    @sso_url || self.class.sso_url\n  end\n\n  def self.parse(payload, sso_secret = nil)\n    sso = new\n    sso.sso_secret = sso_secret if sso_secret\n\n    parsed = Rack::Utils.parse_query(payload)\n    if sso.sign(parsed[\"sso\"]) != parsed[\"sig\"]\n      raise RuntimeError, \"Bad signature for payload\"\n    end\n\n    decoded = Base64.decode64(parsed[\"sso\"])\n    decoded_hash = Rack::Utils.parse_query(decoded)\n\n    ACCESSORS.each do |k|\n      val = decoded_hash[k.to_s]\n      val = val.to_i if FIXNUMS.include? k\n      sso.send(\"#{k}=\", val)\n    end\n    sso\n  end\n\n  def sign(payload)\n    OpenSSL::HMAC.hexdigest(\"sha256\", sso_secret, payload)\n  end\n\n\n  def to_url(base_url=nil)\n    <ins>base </ins><ins>=</ins><ins> </ins>\"#{base_url || sso_url}<ins>\"</ins><ins>\n</ins><ins> </ins><ins> </ins><ins> </ins><ins> </ins><ins>\"</ins><ins>#</ins><ins>{</ins><ins>base</ins><ins>}</ins><ins>#</ins><ins>{</ins><ins>base</ins><ins>.</ins><ins>include</ins>?<ins>(</ins><ins>'</ins><ins>?</ins><ins>'</ins><ins>)</ins><ins> </ins><ins>?</ins><ins> </ins><ins>'</ins><ins>&</ins><ins>'</ins><ins> </ins><ins>:</ins><ins> </ins><ins>'</ins><ins>?</ins><ins>'</ins><ins>}</ins>#{payload}\"\n  end\n\n  def payload\n    payload = Base64.encode64(unsigned_payload)\n    \"sso=#{CGI::escape(payload)}&sig=#{sign(payload)}\"\n  end\n\n  def unsigned_payload\n    payload = {}\n    ACCESSORS.each do |k|\n     next unless (val = send k)\n\n     payload[k] = val\n    end\n\n    Rack::Utils.build_query(payload)\n  end\n\nend\n</code></pre>\n\n\n\n  </article><div style=\"clear: both\"></div>\n</aside></p><p>A trivial implementation would be:</p><p></p><pre><code class=\"lang-auto\">class DiscourseSsoController &lt; ApplicationController\n  def sso\n    secret = \"MY_SECRET_STRING\"\n    sso = SingleSignOn.parse(request.query_string, secret)\n    sso.email = \"user@email.com\"\n    sso.name = \"Bill Hicks\"\n    sso.username = \"bill@hicks.com\"\n    sso.external_id = \"123\" # unique to your application\n    sso.sso_secret = secret\n\n    redirect_to sso.to_url(\"http://l.discourse/session/sso_login\")\n  end\nend</code></pre><h3>Transitioning to and from single sign on.</h3><p>The system always trusts emails provided by the single sign on endpoint. This means that if you had an existing account in the past on Discourse with SSO disabled, SSO will simply re-use it and avoid creating a new account. </p><p>If you ever turn off SSO, users will be able to reset passwords and gain access back to their accounts. </p><h3>Future work</h3><ul>\n<li><p>We would like to gather more reference implementations for SSO on other platforms. If you have one please post to <a href=\"https://meta.discourse.org/category/extensibility/sso\">the Extensibility / SSO category</a>.</p></li>\n<li><p>Add session expiry and/or revalidation logic, so users are not logged in forever. </p></li>\n<li><p>Create an API endpoint to log off users, in case somebody logs off the main site. </p></li>\n<li><p>Consider adding a discourse_sso gem to make it easier to implement in Ruby. </p></li>\n</ul><h2>Updates:</h2><p><strong>2-Feb-2014</strong> </p><ul>\n<li>use <a href=\"http://en.wikipedia.org/wiki/Hash-based_message_authentication_code\">HMAC</a><ins>-</ins><ins>SHA256 </ins><del> </del><del>256 </del>instead of SHA256. This is more secure and cleanly separates key from payload.</li>\n<li>removed return_url, the system will automatically redirect users back to the page they were on after login </li>\n</ul></div>","side_by_side":"<div class=\"span8\"><p>Discourse now ships with official hooks to perform auth offsite. </p><h3>The Problem</h3><p>Many sites wish to integrate with a Discourse site, however want to keep all user registration in a separate site. In such a setup all Login operations should be outsourced to a different site. </p><h3>What if I would like SSO in conjunction with existing auth?</h3><p>The intention around SSO is to replace Discourse authentication, if you would like to add a new provider see existing plugins such as: <a href=\"https://meta.discourse.org/t/vk-com-login-vkontakte/12987\">https://meta.discourse.org/t/vk-com-login-vkontakte/12987</a></p><h3>Enabling SSO</h3><p>To enable single sign on you have 3 settings you need to fill out:</p><p class=\"diff-del diff-del\"></p><div class=\"diff-del lightbox-wrapper\"><a href=\"//meta-discourse.r.worldssl.net/uploads/default/3415/c68d5890b0f7bd4f.png\" class=\"diff-del lightbox\" title=\"Pasted image\"><img src=\"//meta-discourse.r.worldssl.net/uploads/default/_optimized/07c/3bf/3fa1d69ceb_690x207.png\" width=\"690\" height=\"207\" class=\"diff-del\"><div class=\"diff-del meta\"><del>\n</del><span class=\"diff-del filename\"><del>Pasted </del><del>image</del></span class=\"diff-del\"><span class=\"diff-del informations\"><del>798x240 </del><del>16</del><del>.</del><del>8 </del><del>KB</del></span class=\"diff-del\"><span class=\"diff-del expand\"></span class=\"diff-del\"><del>\n</del></div class=\"diff-del\"></a class=\"diff-del\"></div class=\"diff-del\"><p><code>enable_sso</code> : must be enabled, global switch<br><code>sso_url</code>: the <strong>offsite</strong> URL users will be sent to when attempting to log on<br><code>sso_secret</code>: a secret string used to hash SSO payloads. Ensures payloads are authentic.</p><p>Once <code>enable_sso</code> is set to true:</p><ul>\n<li>Clicking on login or avatar will, redirect you to <code>/session/sso</code> which in turn will redirect users to <code>sso_url</code> with a signed payload.</li>\n<li>Users will not be allowed to \"change password\". That field is removed from the user profile.</li>\n<li>Users will no longer be able to use Discourse auth (username/password, google, etc)</li>\n</ul><h3>What if you check it by mistake?</h3><p>If you check <code>enable_sso</code> by mistake and need to revert to the original state and no longer have access to the admin panel</p><p>run:</p><p></p><pre><code class=\"lang-auto\">RAILS_ENV=production bin/rails c\nirb &gt; SiteSetting.enable_sso = false</code></pre><h3>Implementing SSO on your site</h3><p>Discourse will redirect clients to <code>sso_url</code> with a signed payload: (say sso_url is <code>https://somesite.com/sso</code>)</p><p>You will receive incoming traffic with the following</p><p><code>https://somesite.com/sso?sso=PAYLOAD&amp;sig=SIG</code></p><p>The payload is a Base64 encoded string comprising of a <a href=\"http://en.wikipedia.org/wiki/Nonce\">nonce</a>. The payload is always a valid querystring. </p><p>For example, if the nonce is ABCD. raw_payload will be:</p><p><code>nonce=ABCD</code>, this raw payload is <a href=\"http://en.wikipedia.org/wiki/Base64\">base 64</a> encoded.</p><p>The endpoint being called must</p><ol>\n<li>Validate the signature, ensure that <del>HMAC </del><del>256 </del>of sso_secret,PAYLOAD is equal to the sig</li>\n<li>Perform whatever authentication it has to</li>\n<li>Create a new payload with <strong>nonce</strong>, <strong>email</strong>, <strong>external_id</strong> and optionally (username, name, return_url) </li>\n<li>Base64 encode the payload</li>\n<li>Calculate a <del>HMAC </del><del>256 </del>hash of the using sso_secret as the key and Base64 encoded payload as text </li>\n<li>Redirect back to <code>http://discourse_site/session/sso_login?sso=payload&sig=sig</code>\n</li>\n</ol><p>Discourse will validate that the nonce is valid (if valid it will expire it right away so it can no longer be used) it will attempt to:</p><ol>\n<li>Log the user on by looking up an already associated <strong>external_id</strong> in the SingleSignOnRecord model</li>\n<li>Log the user on by using the email provided (updating external_id)</li>\n<li>Create a new account for the user providing (email, username, name) updating external_id</li>\n</ol><h3>Security concerns</h3><p>The nonce (one time token) will expire automatically after 10 minutes. This means that as soon as the user is redirected to your site they have 10 minutes to log in / create a new account. </p><p>The protocol is safe against replay attacks as nonce may only be used once.</p><h3>Reference implementation</h3><p>Discourse contains a reference implementation of the SSO class:</p><p><aside class=\"onebox githubblob\"><header class=\"source\"><a href=\"https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb\">\n      github.com\n    </a>\n  </header><article class=\"onebox-body\"><h4><a href=\"https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb\" target=\"_blank\">https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb</a></h4>\n<pre><code class=\"\">class SingleSignOn\n  ACCESSORS = [:nonce, :name, :username, :email, :about_me, :external_email, :external_username, :external_name, :external_id]\n  FIXNUMS = []\n  NONCE_EXPIRY_TIME = 10.minutes\n\n  attr_accessor(*ACCESSORS)\n  attr_accessor :sso_secret, :sso_url\n\n  def self.sso_secret\n    raise RuntimeError, \"sso_secret not implemented on class, be sure to set it on instance\"\n  end\n\n  def self.sso_url\n    raise RuntimeError, \"sso_url not implemented on class, be sure to set it on instance\"\n  end\n\n  def sso_secret\n    @sso_secret || self.class.sso_secret\n  end\n\n  def sso_url\n    @sso_url || self.class.sso_url\n  end\n\n  def self.parse(payload, sso_secret = nil)\n    sso = new\n    sso.sso_secret = sso_secret if sso_secret\n\n    parsed = Rack::Utils.parse_query(payload)\n    if sso.sign(parsed[\"sso\"]) != parsed[\"sig\"]\n      raise RuntimeError, \"Bad signature for payload\"\n    end\n\n    decoded = Base64.decode64(parsed[\"sso\"])\n    decoded_hash = Rack::Utils.parse_query(decoded)\n\n    ACCESSORS.each do |k|\n      val = decoded_hash[k.to_s]\n      val = val.to_i if FIXNUMS.include? k\n      sso.send(\"#{k}=\", val)\n    end\n    sso\n  end\n\n  def sign(payload)\n    OpenSSL::HMAC.hexdigest(\"sha256\", sso_secret, payload)\n  end\n\n\n  def to_url(base_url=nil)\n    \"#{base_url || sso_url}?#{payload}\"\n  end\n\n  def payload\n    payload = Base64.encode64(unsigned_payload)\n    \"sso=#{CGI::escape(payload)}&sig=#{sign(payload)}\"\n  end\n\n  def unsigned_payload\n    payload = {}\n    ACCESSORS.each do |k|\n     next unless (val = send k)\n\n     payload[k] = val\n    end\n\n    Rack::Utils.build_query(payload)\n  end\n\nend\n</code></pre>\n\n\n\n  </article><div style=\"clear: both\"></div>\n</aside></p><p>A trivial implementation would be:</p><p></p><pre><code class=\"lang-auto\">class DiscourseSsoController &lt; ApplicationController\n  def sso\n    secret = \"MY_SECRET_STRING\"\n    sso = SingleSignOn.parse(request.query_string, secret)\n    sso.email = \"user@email.com\"\n    sso.name = \"Bill Hicks\"\n    sso.username = \"bill@hicks.com\"\n    sso.external_id = \"123\" # unique to your application\n    sso.sso_secret = secret\n\n    redirect_to sso.to_url(\"http://l.discourse/session/sso_login\")\n  end\nend</code></pre><h3>Transitioning to and from single sign on.</h3><p>The system always trusts emails provided by the single sign on endpoint. This means that if you had an existing account in the past on Discourse with SSO disabled, SSO will simply re-use it and avoid creating a new account. </p><p>If you ever turn off SSO, users will be able to reset passwords and gain access back to their accounts. </p><h3>Future work</h3><ul>\n<li><p>We would like to gather more reference implementations for SSO on other platforms. If you have one please post to <a href=\"https://meta.discourse.org/category/extensibility/sso\">the Extensibility / SSO category</a>.</p></li>\n<li><p>Add session expiry and/or revalidation logic, so users are not logged in forever. </p></li>\n<li><p>Create an API endpoint to log off users, in case somebody logs off the main site. </p></li>\n<li><p>Consider adding a discourse_sso gem to make it easier to implement in Ruby. </p></li>\n</ul><h2>Updates:</h2><p><strong>2-Feb-2014</strong> </p><ul>\n<li>use <a href=\"http://en.wikipedia.org/wiki/Hash-based_message_authentication_code\">HMAC</a><del> </del><del>256 </del>instead of SHA256. This is more secure and cleanly separates key from payload.</li>\n<li>removed return_url, the system will automatically redirect users back to the page they were on after login </li>\n</ul></div><div class=\"span8 offset1\"><p>Discourse now ships with official hooks to perform auth offsite. </p><h3>The Problem</h3><p>Many sites wish to integrate with a Discourse site, however want to keep all user registration in a separate site. In such a setup all Login operations should be outsourced to a different site. </p><h3>What if I would like SSO in conjunction with existing auth?</h3><p>The intention around SSO is to replace Discourse authentication, if you would like to add a new provider see existing plugins such as: <a href=\"https://meta.discourse.org/t/vk-com-login-vkontakte/12987\">https://meta.discourse.org/t/vk-com-login-vkontakte/12987</a></p><h3>Enabling SSO</h3><p>To enable single sign on you have 3 settings you need to fill out:</p><p class=\"diff-ins\"><img src=\"/uploads/default/3415/c68d5890b0f7bd4f.png\" width=\"690\" height=\"207\" class=\"diff-ins\"></p class=\"diff-ins\"><p><code>enable_sso</code> : must be enabled, global switch<br><code>sso_url</code>: the <strong>offsite</strong> URL users will be sent to when attempting to log on<br><code>sso_secret</code>: a secret string used to hash SSO payloads. Ensures payloads are authentic.</p><p>Once <code>enable_sso</code> is set to true:</p><ul>\n<li>Clicking on login or avatar will, redirect you to <code>/session/sso</code> which in turn will redirect users to <code>sso_url</code> with a signed payload.</li>\n<li>Users will not be allowed to \"change password\". That field is removed from the user profile.</li>\n<li>Users will no longer be able to use Discourse auth (username/password, google, etc)</li>\n</ul><h3>What if you check it by mistake?</h3><p>If you check <code>enable_sso</code> by mistake and need to revert to the original state and no longer have access to the admin panel</p><p>run:</p><p></p><pre><code class=\"lang-auto\">RAILS_ENV=production bin/rails c\nirb &gt; SiteSetting.enable_sso = false</code></pre><h3>Implementing SSO on your site</h3><p>Discourse will redirect clients to <code>sso_url</code> with a signed payload: (say sso_url is <code>https://somesite.com/sso</code>)</p><p>You will receive incoming traffic with the following</p><p><code>https://somesite.com/sso?sso=PAYLOAD&amp;sig=SIG</code></p><p>The payload is a Base64 encoded string comprising of a <a href=\"http://en.wikipedia.org/wiki/Nonce\">nonce</a>. The payload is always a valid querystring. </p><p>For example, if the nonce is ABCD. raw_payload will be:</p><p><code>nonce=ABCD</code>, this raw payload is <a href=\"http://en.wikipedia.org/wiki/Base64\">base 64</a> encoded.</p><p>The endpoint being called must</p><ol>\n<li>Validate the signature, ensure that <ins>HMAC</ins><ins>-</ins><ins>SHA256 </ins>of sso_secret,<ins> </ins>PAYLOAD is equal to the sig</li>\n<li>Perform whatever authentication it has to</li>\n<li>Create a new payload with <strong>nonce</strong>, <strong>email</strong>, <strong>external_id</strong> and optionally (username, name, return_url) </li>\n<li>Base64 encode the payload</li>\n<li>Calculate a <ins>HMAC</ins><ins>-</ins><ins>SHA256 </ins>hash of the using sso_secret as the key and Base64 encoded payload as text </li>\n<li>Redirect back to <code>http://discourse_site/session/sso_login?sso=payload&sig=sig</code>\n</li>\n</ol><p>Discourse will validate that the nonce is valid (if valid it will expire it right away so it can no longer be used) it will attempt to:</p><ol>\n<li>Log the user on by looking up an already associated <strong>external_id</strong> in the SingleSignOnRecord model</li>\n<li>Log the user on by using the email provided (updating external_id)</li>\n<li>Create a new account for the user providing (email, username, name) updating external_id</li>\n</ol><h3>Security concerns</h3><p>The nonce (one time token) will expire automatically after 10 minutes. This means that as soon as the user is redirected to your site they have 10 minutes to log in / create a new account. </p><p>The protocol is safe against replay attacks as nonce may only be used once.</p><h3>Reference implementation</h3><p>Discourse contains a reference implementation of the SSO class:</p><p><aside class=\"onebox githubblob\"><header class=\"source\"><a href=\"https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb\">\n      github.com\n    </a>\n  </header><article class=\"onebox-body\"><h4><a href=\"https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb\" target=\"_blank\">https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb</a></h4>\n<pre><code class=\"\">class SingleSignOn\n  ACCESSORS = [:nonce, :name, :username, :email, :about_me, :external_email, :external_username, :external_name, :external_id]\n  FIXNUMS = []\n  NONCE_EXPIRY_TIME = 10.minutes\n\n  attr_accessor(*ACCESSORS)\n  attr_accessor :sso_secret, :sso_url\n\n  def self.sso_secret\n    raise RuntimeError, \"sso_secret not implemented on class, be sure to set it on instance\"\n  end\n\n  def self.sso_url\n    raise RuntimeError, \"sso_url not implemented on class, be sure to set it on instance\"\n  end\n\n  def sso_secret\n    @sso_secret || self.class.sso_secret\n  end\n\n  def sso_url\n    @sso_url || self.class.sso_url\n  end\n\n  def self.parse(payload, sso_secret = nil)\n    sso = new\n    sso.sso_secret = sso_secret if sso_secret\n\n    parsed = Rack::Utils.parse_query(payload)\n    if sso.sign(parsed[\"sso\"]) != parsed[\"sig\"]\n      raise RuntimeError, \"Bad signature for payload\"\n    end\n\n    decoded = Base64.decode64(parsed[\"sso\"])\n    decoded_hash = Rack::Utils.parse_query(decoded)\n\n    ACCESSORS.each do |k|\n      val = decoded_hash[k.to_s]\n      val = val.to_i if FIXNUMS.include? k\n      sso.send(\"#{k}=\", val)\n    end\n    sso\n  end\n\n  def sign(payload)\n    OpenSSL::HMAC.hexdigest(\"sha256\", sso_secret, payload)\n  end\n\n\n  def to_url(base_url=nil)\n    <ins>base </ins><ins>=</ins><ins> </ins>\"#{base_url || sso_url}<ins>\"</ins><ins>\n</ins><ins> </ins><ins> </ins><ins> </ins><ins> </ins><ins>\"</ins><ins>#</ins><ins>{</ins><ins>base</ins><ins>}</ins><ins>#</ins><ins>{</ins><ins>base</ins><ins>.</ins><ins>include</ins>?<ins>(</ins><ins>'</ins><ins>?</ins><ins>'</ins><ins>)</ins><ins> </ins><ins>?</ins><ins> </ins><ins>'</ins><ins>&</ins><ins>'</ins><ins> </ins><ins>:</ins><ins> </ins><ins>'</ins><ins>?</ins><ins>'</ins><ins>}</ins>#{payload}\"\n  end\n\n  def payload\n    payload = Base64.encode64(unsigned_payload)\n    \"sso=#{CGI::escape(payload)}&sig=#{sign(payload)}\"\n  end\n\n  def unsigned_payload\n    payload = {}\n    ACCESSORS.each do |k|\n     next unless (val = send k)\n\n     payload[k] = val\n    end\n\n    Rack::Utils.build_query(payload)\n  end\n\nend\n</code></pre>\n\n\n\n  </article><div style=\"clear: both\"></div>\n</aside></p><p>A trivial implementation would be:</p><p></p><pre><code class=\"lang-auto\">class DiscourseSsoController &lt; ApplicationController\n  def sso\n    secret = \"MY_SECRET_STRING\"\n    sso = SingleSignOn.parse(request.query_string, secret)\n    sso.email = \"user@email.com\"\n    sso.name = \"Bill Hicks\"\n    sso.username = \"bill@hicks.com\"\n    sso.external_id = \"123\" # unique to your application\n    sso.sso_secret = secret\n\n    redirect_to sso.to_url(\"http://l.discourse/session/sso_login\")\n  end\nend</code></pre><h3>Transitioning to and from single sign on.</h3><p>The system always trusts emails provided by the single sign on endpoint. This means that if you had an existing account in the past on Discourse with SSO disabled, SSO will simply re-use it and avoid creating a new account. </p><p>If you ever turn off SSO, users will be able to reset passwords and gain access back to their accounts. </p><h3>Future work</h3><ul>\n<li><p>We would like to gather more reference implementations for SSO on other platforms. If you have one please post to <a href=\"https://meta.discourse.org/category/extensibility/sso\">the Extensibility / SSO category</a>.</p></li>\n<li><p>Add session expiry and/or revalidation logic, so users are not logged in forever. </p></li>\n<li><p>Create an API endpoint to log off users, in case somebody logs off the main site. </p></li>\n<li><p>Consider adding a discourse_sso gem to make it easier to implement in Ruby. </p></li>\n</ul><h2>Updates:</h2><p><strong>2-Feb-2014</strong> </p><ul>\n<li>use <a href=\"http://en.wikipedia.org/wiki/Hash-based_message_authentication_code\">HMAC</a><ins>-</ins><ins>SHA256 </ins>instead of SHA256. This is more secure and cleanly separates key from payload.</li>\n<li>removed return_url, the system will automatically redirect users back to the page they were on after login </li>\n</ul></div>","side_by_side_markdown":"<table class=\"markdown\"><tr><td>Discourse now ships with official hooks to perform auth offsite. \n\n### The Problem\n\nMany sites wish to integrate with a Discourse site, however want to keep all user registration in a separate site. In such a setup all Login operations should be outsourced to a different site. \n\n### What if I would like SSO in conjunction with existing auth?\n\nThe intention around SSO is to replace Discourse authentication, if you would like to add a new provider see existing plugins such as: https://meta.discourse.org/t/vk-com-login-vkontakte/12987\n\n### Enabling SSO\n\nTo enable single sign on you have 3 settings you need to fill out:\n\n&lt;img src=&quot;/uploads/default/3415/c68d5890b0f7bd4f.png&quot; width=&quot;690&quot; height=&quot;207&quot;&gt; \n\n`enable_sso` : must be enabled, global switch\n`sso_url`: the **offsite** URL users will be sent to when attempting to log on\n`sso_secret`: a secret string used to hash SSO payloads. Ensures payloads are authentic.\n\nOnce `enable_sso` is set to true:\n\n- Clicking on login or avatar will, redirect you to `/session/sso` which in turn will redirect users to `sso_url` with a signed payload.\n- Users will not be allowed to &quot;change password&quot;. That field is removed from the user profile.\n- Users will no longer be able to use Discourse auth (username/password, google, etc)\n\n### What if you check it by mistake?\n\nIf you check `enable_sso` by mistake and need to revert to the original state and no longer have access to the admin panel\n\nrun:\n\n```\nRAILS_ENV=production bin/rails c\nirb &gt; SiteSetting.enable_sso = false\n```\n\n### Implementing SSO on your site\n\nDiscourse will redirect clients to `sso_url` with a signed payload: (say sso_url is `https://somesite.com/sso`)\n\nYou will receive incoming traffic with the following\n\n`https://somesite.com/sso?sso=PAYLOAD&amp;sig=SIG`\n\nThe payload is a Base64 encoded string comprising of a [nonce][1]. The payload is always a valid querystring. \n\nFor example, if the nonce is ABCD. raw_payload will be:\n\n`nonce=ABCD`, this raw payload is [base 64][2] encoded.\n\nThe endpoint being called must\n\n</td><td>Discourse now ships with official hooks to perform auth offsite. \n\n### The Problem\n\nMany sites wish to integrate with a Discourse site, however want to keep all user registration in a separate site. In such a setup all Login operations should be outsourced to a different site. \n\n### What if I would like SSO in conjunction with existing auth?\n\nThe intention around SSO is to replace Discourse authentication, if you would like to add a new provider see existing plugins such as: https://meta.discourse.org/t/vk-com-login-vkontakte/12987\n\n### Enabling SSO\n\nTo enable single sign on you have 3 settings you need to fill out:\n\n&lt;img src=&quot;/uploads/default/3415/c68d5890b0f7bd4f.png&quot; width=&quot;690&quot; height=&quot;207&quot;&gt; \n\n`enable_sso` : must be enabled, global switch\n`sso_url`: the **offsite** URL users will be sent to when attempting to log on\n`sso_secret`: a secret string used to hash SSO payloads. Ensures payloads are authentic.\n\nOnce `enable_sso` is set to true:\n\n- Clicking on login or avatar will, redirect you to `/session/sso` which in turn will redirect users to `sso_url` with a signed payload.\n- Users will not be allowed to &quot;change password&quot;. That field is removed from the user profile.\n- Users will no longer be able to use Discourse auth (username/password, google, etc)\n\n### What if you check it by mistake?\n\nIf you check `enable_sso` by mistake and need to revert to the original state and no longer have access to the admin panel\n\nrun:\n\n```\nRAILS_ENV=production bin/rails c\nirb &gt; SiteSetting.enable_sso = false\n```\n\n### Implementing SSO on your site\n\nDiscourse will redirect clients to `sso_url` with a signed payload: (say sso_url is `https://somesite.com/sso`)\n\nYou will receive incoming traffic with the following\n\n`https://somesite.com/sso?sso=PAYLOAD&amp;sig=SIG`\n\nThe payload is a Base64 encoded string comprising of a [nonce][1]. The payload is always a valid querystring. \n\nFor example, if the nonce is ABCD. raw_payload will be:\n\n`nonce=ABCD`, this raw payload is [base 64][2] encoded.\n\nThe endpoint being called must\n\n</td></tr><tr><td class=\"diff-del\">1. Validate the signature, ensure that <del>HMAC 256 </del>of sso_secret,PAYLOAD is equal to the sig\n</td><td class=\"diff-ins\">1. Validate the signature, ensure that <ins>HMAC-SHA256 </ins>of sso_secret,<ins> </ins>PAYLOAD is equal to the sig\n</td></tr><tr><td>2. Perform whatever authentication it has to\n3. Create a new payload with **nonce**, **email**, **external_id** and optionally (username, name, return_url) \n4. Base64 encode the payload\n</td><td>2. Perform whatever authentication it has to\n3. Create a new payload with **nonce**, **email**, **external_id** and optionally (username, name, return_url) \n4. Base64 encode the payload\n</td></tr><tr><td class=\"diff-del\">5. Calculate a <del>HMAC 256 </del>hash of the using sso_secret as the key and Base64 encoded payload as text \n</td><td class=\"diff-ins\">5. Calculate a <ins>HMAC-SHA256 </ins>hash of the using sso_secret as the key and Base64 encoded payload as text \n</td></tr><tr><td>6. Redirect back to `http://discourse_site/session/sso_login?sso=payload&amp;sig=sig`\n\nDiscourse will validate that the nonce is valid (if valid it will expire it right away so it can no longer be used) it will attempt to:\n\n1. Log the user on by looking up an already associated **external_id** in the SingleSignOnRecord model\n2. Log the user on by using the email provided (updating external_id)\n3. Create a new account for the user providing (email, username, name) updating external_id\n\n###Security concerns\n\nThe nonce (one time token) will expire automatically after 10 minutes. This means that as soon as the user is redirected to your site they have 10 minutes to log in / create a new account. \n\nThe protocol is safe against replay attacks as nonce may only be used once.\n\n###Reference implementation\n\nDiscourse contains a reference implementation of the SSO class:\n\nhttps://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb\n\nA trivial implementation would be:\n\n```\nclass DiscourseSsoController &lt; ApplicationController\n  def sso\n    secret = &quot;MY_SECRET_STRING&quot;\n    sso = SingleSignOn.parse(request.query_string, secret)\n    sso.email = &quot;user@email.com&quot;\n    sso.name = &quot;Bill Hicks&quot;\n    sso.username = &quot;bill@hicks.com&quot;\n    sso.external_id = &quot;123&quot; # unique to your application\n    sso.sso_secret = secret\n\n    redirect_to sso.to_url(&quot;http://l.discourse/session/sso_login&quot;)\n  end\nend\n```\n\n### Transitioning to and from single sign on.\n\nThe system always trusts emails provided by the single sign on endpoint. This means that if you had an existing account in the past on Discourse with SSO disabled, SSO will simply re-use it and avoid creating a new account. \n\nIf you ever turn off SSO, users will be able to reset passwords and gain access back to their accounts. \n\n### Future work\n\n- We would like to gather more reference implementations for SSO on other platforms. If you have one please post to [the Extensibility / SSO category](https://meta.discourse.org/category/extensibility/sso).\n\n- Add session expiry and/or revalidation logic, so users are not logged in forever. \n\n- Create an API endpoint to log off users, in case somebody logs off the main site. \n\n- Consider adding a discourse_sso gem to make it easier to implement in Ruby. \n\n\n## Updates:\n\n**2-Feb-2014** \n\n</td><td>6. Redirect back to `http://discourse_site/session/sso_login?sso=payload&amp;sig=sig`\n\nDiscourse will validate that the nonce is valid (if valid it will expire it right away so it can no longer be used) it will attempt to:\n\n1. Log the user on by looking up an already associated **external_id** in the SingleSignOnRecord model\n2. Log the user on by using the email provided (updating external_id)\n3. Create a new account for the user providing (email, username, name) updating external_id\n\n###Security concerns\n\nThe nonce (one time token) will expire automatically after 10 minutes. This means that as soon as the user is redirected to your site they have 10 minutes to log in / create a new account. \n\nThe protocol is safe against replay attacks as nonce may only be used once.\n\n###Reference implementation\n\nDiscourse contains a reference implementation of the SSO class:\n\nhttps://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb\n\nA trivial implementation would be:\n\n```\nclass DiscourseSsoController &lt; ApplicationController\n  def sso\n    secret = &quot;MY_SECRET_STRING&quot;\n    sso = SingleSignOn.parse(request.query_string, secret)\n    sso.email = &quot;user@email.com&quot;\n    sso.name = &quot;Bill Hicks&quot;\n    sso.username = &quot;bill@hicks.com&quot;\n    sso.external_id = &quot;123&quot; # unique to your application\n    sso.sso_secret = secret\n\n    redirect_to sso.to_url(&quot;http://l.discourse/session/sso_login&quot;)\n  end\nend\n```\n\n### Transitioning to and from single sign on.\n\nThe system always trusts emails provided by the single sign on endpoint. This means that if you had an existing account in the past on Discourse with SSO disabled, SSO will simply re-use it and avoid creating a new account. \n\nIf you ever turn off SSO, users will be able to reset passwords and gain access back to their accounts. \n\n### Future work\n\n- We would like to gather more reference implementations for SSO on other platforms. If you have one please post to [the Extensibility / SSO category](https://meta.discourse.org/category/extensibility/sso).\n\n- Add session expiry and/or revalidation logic, so users are not logged in forever. \n\n- Create an API endpoint to log off users, in case somebody logs off the main site. \n\n- Consider adding a discourse_sso gem to make it easier to implement in Ruby. \n\n\n## Updates:\n\n**2-Feb-2014** \n\n</td></tr><tr><td class=\"diff-del\">- use [HMAC][3]<del> 256 </del>instead of SHA256. This is more secure and cleanly separates key from payload.\n</td><td class=\"diff-ins\">- use [HMAC][3]<ins>-SHA256 </ins>instead of SHA256. This is more secure and cleanly separates key from payload.\n</td></tr><tr><td>- removed return_url, the system will automatically redirect users back to the page they were on after login \n\n\n  [1]: http://en.wikipedia.org/wiki/Nonce\n  [2]: http://en.wikipedia.org/wiki/Base64\n  [3]: http://en.wikipedia.org/wiki/Hash-based_message_authentication_code</td><td>- removed return_url, the system will automatically redirect users back to the page they were on after login \n\n\n  [1]: http://en.wikipedia.org/wiki/Nonce\n  [2]: http://en.wikipedia.org/wiki/Base64\n  [3]: http://en.wikipedia.org/wiki/Hash-based_message_authentication_code</td></tr></table>"},"title_changes":null,"category_changes":null}
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement