External SSO unable to get signature match

Hi All,

We are trying to get discourse working with an external SSO endpoint but can’t seem to get the signature to match.

The logs show:

[2020-12-23 21:10:50 +0000] [7] [DEBUG] 2d9c3e07e2e7551e51af471ca3b50e83d06411b2c75e443f96def5b6a257d141
[2020-12-23 21:10:50 +0000] [7] [DEBUG] ad02fdc5bd85adce7722bc264352cca9b931392735ed72474966c4461f188b8c
[2020-12-23 21:10:50 +0000] [7] [DEBUG] compare_digest is different

What are we doing wrong? Does the hmac preparation look okay?

endpoint:
@app.route(/discourse)
def discourse():
    try:
        cookie = request.cookies['__session']
    except KeyError:
        logging.error("Session cookie was not present")
        return redirect(login_url)
    
    try:
        decoded_token = verify_session_cookie(cookie, check_revoked=True)
    except ValueError:
        return {"message": "Session cookie was present but not a valid string or empty"}, 400
    except ExpiredSessionCookieError:
        return {"message": "Session cookie was present but expired."}, 400
    
    try:
        uid = decoded_token['uid']
    except KeyError:
        return {"message": "The user id could not be read from the decoded token"}, 500
    
    try:
        payload = request.args['sso']
    except KeyError:
        return {"message": "Payload parameter was not present"}, 400
    
    try:
        sig = request.args['sig']
    except KeyError:
        return {"message": "Sig parameter was not present"}, 400
    
    try:
        nonce = validate_sso_and_generate_nonce(payload, sig, sso_secret)
    except DiscourseError as e:
        current_app.logger.error("sso validation failed: " + str(e))
        return redirect(login_url)
    
    user = get_user(uid)
    sso_url = generate_sso_redirect_url(forum_url, nonce, sso_secret, user.email, uid, user.display_name, user.display_name)
    current_app.logger.debug("Discourse endpoint complete")
    return redirect(sso_url)

Where we generate the signature:

def validate_sso_and_generate_nonce(payload, signature, secret):
    """
        payload: provided by Discourse HTTP call to your SSO endpoint as sso GET param
        signature: provided by Discourse HTTP call to your SSO endpoint as sig GET param
        secret: the secret key you entered into Discourse sso secret
        return value: The nonce used by discourse to validate the redirect URL
    """
    if None in [payload, signature]:
        raise DiscourseError('No SSO payload or signature.')

if not secret:
    raise DiscourseError('Invalid secret..')

if not payload:
    raise DiscourseError('Invalid payload..')

payload = bytes(parse.unquote(payload), encoding='utf-8')
decoded = base64.decodestring(payload).decode('utf-8')
if len(payload) == 0 or 'nonce' not in decoded:
    raise DiscourseError('Invalid payload..')

h = hmac.new(base64.b64encode(bytes(secret, encoding='utf-8')), payload, digestmod=hashlib.sha256)
this_signature = h.hexdigest()

current_app.logger.debug(this_signature)
current_app.logger.debug(signature)

#if this_signature != signature:
#    raise DiscourseError('Payload does not match signature.')

if not hmac.compare_digest(this_signature, signature):
    current_app.logger.debug("compare_digest is different")

nonce = decoded.split('=')[1]

return nonce
2 Likes

Have you been able to get this to work? If not, you might be able to see where things are going wrong by having a look at this code:

https://github.com/bennylope/pydiscourse/blob/master/pydiscourse/sso.py#L39

2 Likes

Whilst there are some differences in the preparation of the payload for the HMAC function this code fails with the same issue that the signatures don’t match.