No internet connection
  1. Home
  2. Documentation
  3. How To

How to add Blog Comments Single Sign-On (SSO)

By KajMagnus @KajMagnus2021-07-13 17:53:13.118Z2021-07-16 13:34:24.155Z

You can add Single Sign-On to your blog or website in two ways, but only the authn token way is currently supported (the last one in the list):

  1. Via OpenID Connect (OIDC), but this is not yet supported together with embedded comments. Instead:
  2. By embedding user information (username, user id, email address) in something called an authentication token in the HTML pages in your blog.

Not now, but later, you'll be able to combined those ways to login, with other ways to log in as well — maybe you want SSO for people in your own organization, and also want to let external users add comments using their private Gmail addresses or something.

Blog comments SSO via OIDC

Not yet supported.

Blog comments SSO via Authentication Token

You'll need to write some source code (!). Here's one of Talkyard's automatic tests that you can look at — it does the same things as the instructions below:

You'll also need to already have configured SSO for the Talkyard site when it is accessed directly (not in an embedded comments <iframe>), see: Talkyard Single Sign-On API

The instructions:

  1. You'll need a PASETO token library. Pick one that suits you, here: https://paseto.io, depending on which programming language your website uses.
    (Talkyard uses JPaseto, and also, for automatic tests, paseto.js. If your website is in PHP, then there's this library for you: paragonie/paseto which you'll also find in the list over at paseto.io.)

  2. As admin, go to Settings | Signup and Login in the Admin Area (/-/admin/settings/login).

  3. Scroll down below the Your custom Single Sign-On (SSO) header, down, down, to the Embedded Comments SSO Token Secret setting. Click the Generate new button.

  4. Copy the generated secret. It looks like so: hex:123abcdef... and is a symmetric encryption key. Don't share it with anyone — it is secret.

  5. On your server, generate JSON for the user, should look like this: (here in Javascript, so simpler to read)

    const authnMessage = {
      exp: '2021-07-01T00:00:00+00:00',  // when the authn token expires, you can
                                          // set it to maybe half an hour into the future
      data: {
        user: {
          ssoId: 'the-visitors-unique-id-in-your-website',
          username: 'his_her_username_in_your_website',
          fullName: 'His or her Full Name',
          primaryEmailAddress: 'his-or-her-email-address@x.co',   // <— email addr
          isEmailAddressVerified: true,
        }
      }
    }
    

    Note that you must have verified that the email address above, is really the person's own email address.

  6. Encrypt that JSON in a PASETO v2.local token, using the secret you generated earlier. Here you need to write some code. For example, in Javascript:

    // Just once somewhere in your server code:
    // pasetoV2LocalSecret should be the secret you generated earlier — it starts with 'hex:' which we need to remove.
    const pasetoV2LocalSecretNoHexPrefix = pasetoV2LocalSecret.replace(/^hex:/, '');
    sharedSecretKeyBytes = Buffer.from(pasetoV2LocalSecretNoHexPrefix, 'hex');
    
    // Then, when generating a HTML page with comments:
    const messageAsString = JSON.stringify(authnMessage);
    const sharedSecretKey  = new Paseto.SymmetricKey(new Paseto.V2());
    const token = await sharedSecretKey.inject(sharedSecretKeyBytes).then(() => {
      const encoder = sharedSecretKey.protocol();
      return encoder.encrypt(messageAsString, sharedSecretKey);
    }).then(token => {
      console.log(`Generated PASETO token:  ${token}`);
      // E.g. "v2.local.kBENRnu2p2.....JKJZB9Lw"
      return 'paseto:' + token;
    });;
    
  7. Include that PASETO token, prefixed with paseto:, in the blog posts (or website pages) so Talkyard sees it:

    <script>
    talkyardAuthnToken = 'paseto:v2.local.112233AABBCC......`;
    talkyardServerUrl =  ....
    </script>
    

    You'll need to generate different HTML for different logged in users — so they'll get their own token, and get logged in to Talkyard as themselves.

Finally, reload a blog post with comments, and you should be automatically logged in, in the comments section (if you were logged in at your blog / website already).

  • 18 replies

There are 18 replies. Estimated reading time: 18 minutes

  1. F
    @fas2021-07-16 09:30:02.216Z

    Hi Kaj,

    Thanks for this feature :-).

    I am busy doing the integration work and have some problems:

    1. I am not sure on the shared secret length and format.
      I first generate a shared key in Ty web UI. This is a long string prefixed by "hex:". "hex:b808ec061dd2eb25d82aaa316c4d44e85fb266a261bf69972e4046e222e0e699"
      Do I use this string a) as is, b) strip the hex: prefix and use or, c) strip the hex: prefix and then re-encode as hex (even though it already is hex)?
      Then prefix that with "paseto:" and pass to the token builder as the shared key?
      When I use this long generted shared key and create the token in Java using JPaseto:
        return Pasetos.V2.LOCAL.builder()
               .setSharedSecret(Keys.secretKey(talkyardSsoTokenSecret.getBytes(StandardCharsets.UTF_8)))
               .setSubject(subject)
               .compact();
    

    I get an error saying that the secret key exceeds the maximum 32 characters. Is the shared key limited to 32 length? I reduced my shared key length to keep it at length 32 for my further tests.

    The second problem I am facing is when embedding the comments: I set the variable talkyardAuthnToken with the token contents - which start with 'v2.local.1....' and looks correct even if the contents are wrong. The embedded comments iframe renders, with Add Comment and Login buttons. When clicking Add Comment it requests a login. So I assume that the token login failed. My question is: how can I see the logs of where/how the token login failed?

    Thanks for the help.
    Franz

    1. KajMagnus @KajMagnus2021-07-16 12:05:38.322Z2021-07-16 13:35:02.491Z

      Hi Franz, thanks for the questions (& gives me ideas about how to improve the docs :- ))

      Remove hex:, and keep the remaining string as is. That is, case b).
      (Details: hex: is added by Talkyard — I'm thinking that in other languages than Java, maybe it can be more convenient to get the secret key in Base36 or Base64 or something else, and then there could be e.g. a base36: prefix instead. But the hex decode functions don't want any prefix though.)

      To decode the secret (after having removed hex:), you need to interpret the string as hex — but this: getBytes(StandardCharsets.UTF_8) instead encodes the string as UTF-8 bytes.
      You can use for exampleorg.apache.commons.codec.binary.Hex.decodeHex(...) to do that.
      Here's how Talkyard does it: https://github.com/debiki/talkyard/blob/master/app/talkyard/server/security/PasetoSec.scala#L72

      if (secretStr.startsWith("hex:")) {     // it always does, currently
        val hexStr = secretStr.drop("hex:".length)
        val hexBytes: Array[i8] = org.apache.commons.codec.binary.Hex.decodeHex(hexStr)
        val key: javax.crypto.SecretKey = dev.paseto.jpaseto.lang.Keys.secretKey(hexBytes)
      

      Is the shared key limited to 32 length?

      Yes, it should be exactly 32 bytes.
      (The key length i defined in RFC-8439 about ChaCha20 encryption, and PASETO v2.local uses ChaCha20.)

      When encoding the secret key as utf-8 bytes (instead of parsing it as hex chars into bytes), then you'll get more than 32 bytes and hence a key-too-long error.

      The paseto: prefix (oops I forgot to mention it in the docs above, now fixed) — that should be prefixed only to the Javascript variable talkyardAuthnToken, nowhere else. (JPaseto doesn't want it.) It's for Talkyard, so Ty knows what type of token it is — I'm thinking that maybe in the future there'll be other nice standards in addition to PASETO.

      I set the variable talkyardAuthnToken with the token contents - which start with 'v2.local.1....'

      It should be set to: paseto:v2.local......

      how can I see the logs of where/how the token login failed?

      Currently you need to keep DevTools open (in Chrome or Firefox), open the Network panel, and reload the page. Then you'll see a request to /-/v0/upsert-user-and-login and in the response from the Talkyard server, there'll be an error message. — But it'd be good if Talkyard logged a message about what went wrong, in the browser console. So one can see in DevTools. ... Hmm, Ty does log to the console but that's in the embedded comments iframe, and not so easy to find, in DevTools. This could be improved. It'd also be nice, I think, if there was a place in the Admin Area where one could see such errors. (Maybe it'd be nice, also, with a way for admins to decode the PASETO tokens, just for troubleshooting. A SSO troubleshooting Admin Area page maybe)

      1. KajMagnus @KajMagnus2021-07-16 13:46:56.241Z

        B.t.w. @fas don't forget to generate a new secret :- ) Since the old one is above: hex:b808ec061dd2e...

        1. In reply toKajMagnus:
          F@fas2021-07-16 13:52:17.203Z

          Hi Kaj,

          Thanks for the clarifications. I think I now have the token properly created, but I still dont get logged in and I do not see the call to /-/v0/upsert-user-and-login

          Is all that is necessary to define the variable talkyardAuthnToken and the toekn login will take place?
          talkyardAuthnToken = 'paseto:v2.local.112233AABBCC......`;

          Also, must the user already exist in Ty or will they be automatically created on receiving the token? I am assuming the latter and this may be my problem?

          1. KajMagnus @KajMagnus2021-07-16 16:09:27.186Z

            they be automatically created on receiving the token?

            Yes they'll get created if they don't exist already. (All user details (username, email and the identity ID (the ssoId field)) are included in the PASETO token.)

            Your website doesn't happen to be online somewhere? (Then I could have a look — if you want to, you could sent me a PM)

            In any case, I'll add more log messages so it'll be simpler to investigate.

            1. In reply tofas:
              F@fas2021-07-17 10:11:57.792Z

              Great :-). I got this working and its looking good!

              The issues were just with the way I handle the javascript variables - because I am working from within a polymer web component. On the java side I had to fiddle a bit to get the token right. Here is the working code:

                  String signedToken = "paseto:" + Pasetos.V2.LOCAL.builder()
                          .setSharedSecret(decodeHex(talkyardSsoTokenSecret))
                          .claim("data", Map.of(
                                  "user", Map.of(
                                          "ssoId", customerIdentity,
                                          "username", customerEmail,
                                          "fullname", customerName,
                                          "primaryEmailAddress", customerEmail,
                                          "isEmailAddressVerified", true))
                          )
                          .setExpiration(Instant.now().plus(48, ChronoUnit.HOURS))
                          .compact();
              
              private SecretKey decodeHex(String sharedSecret) {
                  SecretKey secretKey = null;
                  if (sharedSecret.startsWith("hex:")) {      // Strip leading 'hex:'
                      try {
                          sharedSecret = sharedSecret.substring("hex:".length());
                          secretKey = Keys.secretKey(Hex.decodeHex(sharedSecret));
                      } catch (DecoderException e) {
                          log.error(e.getMessage(), e);
                      }
                  }
                  return secretKey;
              }
              1. KajMagnus @KajMagnus2021-07-20 05:38:55.384Z

                Ok :- ) Thanks for posting the sample code. I think 48 hours is a bit much — if the visitor downloads the blog post, and emails to someone, then, the token is in the source, and that other person might get logged in as the visitor. For example, in Disqus, such Single Sign-On data expires after two hours.

                Later, Talkyard will make use of the authn token directly, and won't allow reusing it — however, theoretically, the Ty servers might be offline for a while, and then the token would stay valid.

                Maybe it'd be better to keep the token in a cookie instead, as an alternative. There could be a variable talkyardAuthnTokenCookieName = ... maybe, and then Talkard could get the token from that cookie (that is, your server would set a not-HttpOnly cookie, which the Talkyard script could access).

                1. F@fas2021-07-24 06:53:17.223Z

                  Ok, thanks. I didn't realise that there was an issue here. I will use the recommended 2 hours. But I'm not too concerned about this happening.

              2. In reply tofas:
                F@fas2021-07-17 10:13:07.786Z

                Can you also please show me the css needed to supress the Logout button. Thx.

                1. KajMagnus @KajMagnus2021-07-20 05:23:45.756Z

                  This'll hide the logout button:

                  .dw-a-logout {
                    display: none;
                  }
                  

                  Alternatively, you could configure a Logout URL — then, if one clicks Log Out, Talkyard redirects the browser to a URL of your choice, for example https://your-website/logout-page, where you can log the user out from your website — and then s/he will be logged out from both Talkyard and your website, since when the user returns to the blog, there won't be any authn token in the HTML source.

                  1. F@fas2021-07-24 06:53:38.989Z

                    Thanks for this. Will try it out now.

                    1. F@fas2021-07-25 12:56:19.298Z

                      Where do I put this css snippet? I cannot put it on my site as the comments are rendered in an iframe. Is there some place where one can put custom css in the forum settings?

                      1. KajMagnus @KajMagnus2021-07-26 08:01:14.667Z

                        Here, at your Talkyard site: https:// your talkyard server /-/admin/customize/css-js. That CSS affects the things inside the iframe.

          2. F
            In reply toKajMagnus:
            @fas2021-07-17 12:00:07.982Z

            Hi Kaj, will it also possible to auto-login into a message board/forum in an iframe using the token method?
            Thx,
            Franz

            1. KajMagnus @KajMagnus2021-07-20 05:41:56.965Z

              Some day, yes, however currently only embedded comments are supported.

              Placing the whole forum in an iframe, is currently not supported. I wonder what are good ways to handle navigation in the forum, when it's in an iframe — the main window URL wouldn't change, if jumping between different topics in the iframe. Unless the embedding code updates the main window URL via History.pushState().

              1. F@fas2021-07-24 07:09:42.321Z

                Hi Kaj. This would be an important feature for me. Is there maybe a way I could sponsor some development here?

                I am also not sure why it would be important to have the host window address bar change? I think in my use case this would not be a big problem. It would only be useful for bookmarking a page or is there some other use case?

                I think there would be two ways to access my forum. 1) embedded in the training course in an iframe and then 2) direct access when someone clicks on the reply link in an email, ie no iframe.

                In 1) it would be great to have the sso token login to avoid a duplicate login step and 2) would use the usual sso redirect login.

                1. KajMagnus @KajMagnus2021-07-27 09:04:12.081Z

                  I can have a look and see how much / little work that'd be.

                  why it would be important to have the host window address bar change?
                  It would only be useful for bookmarking a page

                  Yes, for bookmarking a page, and copying a link and sending to someone else. So s/he can open the link in the browser and see the same discussion in the iframe.

                  Maybe it's better to not think about that, in the beginning. And just leave the address bar unchanged.

                  I suppose it could sometimes even be confusing, if the address changed.

                  I think there would be two ways to access my forum 1 ... 2 ...

                  Yes.

                  In 1) it would be great to have the sso token login to avoid a duplicate login step

                  Yes, so it works in the same way, as embedded comments, right. (I mean, auto login via embedded token variable or cookie)

                  1. F@fas2021-07-28 16:56:38.547Z

                    Hi Kaj.
                    I have got a bit further with my integration and am getting close to the finish line now and will be in production soon.

                    I have implemented the normal SSO, as well as the blog comments SSO token.

                    I have now also implemented the forum as a standalone page and as an embedded iframe using the normal SSO.

                    For standalone the SSO works fine, although the user has to click login again. Once login is clicked the SSO process starts and the session detects the user is already logged in and logs the user in immediately. The only nuisance here is that the user has to click the login button. Maybe if there was a way to start the page with the login process immediately requested? I have tried to check the box that the forum read permissions are private (which is what I actually need) but this is not checkable in the admin settings because an error pops up that this is not allowed if blog comments are also used in the forum.

                    For the embedded forum, the SSO process unfortunately fails. The user is redirected to my login page, is automatically logged in, and upon redirect back to the forum, the forum still shows the user not logged in. I think there may be some problem here with same site permissions or something like that, because exactly the same code and process works for the standalone forum page.

                    I will go in production with the standalone forum which is usable. So probably more important for me now than the embedded iframe forum is to
                    a) be able to create a forum using the API (in addition to categories and pages) and
                    b) to be able to programmatically change the name of the blog comments category as it appears in the forum. Remember we discussed this possibility as either an attribute on the html element, or as a field in the SSO token - either would work for me as I am not concerned about malicious name changing.

                    Thanks again for all your help and support on this project.