1

I am currently developing a quiz game to work as an external tool within Moodle. I am following the IMS LTI specification and using OAuth for authentication, as it requires.

I recently managed to authenticate the launch POST request from Moodle, and am now trying to send grades back to Moodle from my tool by using LTI Basic Outcome Service.

Here is where I have encountered a problem: I build POX messages, sign the request, and send it, but for some reason they are succeeding just once every several attempts (30% of the time, more or less). For the rest of them, Moodle responds with a failure POX message that includes "Message signature not valid" as its description.

Obviously, I want to trust my requests will succeed whenever I need them to.

For illustration purposes, I show you here below an example of a successful request and a failed one, along with each one's Base String (the one OAuth takes for signing):

For the successful request:

  1. Base String of the request (line breaks are for display purposes only):

POST&http%3A%2F%2F127.0.0.1%2Fmoodle%2Fmod%2Flti%2Fservice.php &oauth_body_hash%3DhPssgohenJEvtKta2so7Y27p3kU%253D%26oauth_callback%3Dabout%253Ablank %26oauth_consumer_key%3Dkey%26oauth_nonce%3D63cc0764c4cc4701abe28fa5fd406378 %26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1525940779%26oauth_version%3D1.0

  1. Body of the response:

    <?xml version="1.0" encoding="UTF-8"?>
    <imsx_POXEnvelopeResponse xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
        <imsx_POXHeader>
            <imsx_POXResponseHeaderInfo>
                <imsx_version>V1.0</imsx_version>
                <imsx_messageIdentifier>1602471533</imsx_messageIdentifier>
                <imsx_statusInfo>
                    <imsx_codeMajor>success</imsx_codeMajor>
                    <imsx_severity>status</imsx_severity>
                    <imsx_description>Result read</imsx_description>
                    <imsx_messageRefIdentifier>1339905165</imsx_messageRefIdentifier>
                    <imsx_operationRefIdentifier>readResultRequest</imsx_operationRefIdentifier>
                </imsx_statusInfo>
            </imsx_POXResponseHeaderInfo>
        </imsx_POXHeader>
        <imsx_POXBody>
            <readResultResponse>
                <result>
                    <resultScore>
                        <language>en</language>
                        <textString>0.5</textString>
                    </resultScore>
                </result>
            </readResultResponse>
        </imsx_POXBody>
    </imsx_POXEnvelopeResponse>
    

For the failed request:

  1. Base String of the request (line breaks are for display purposes only):

POST&http%3A%2F%2F127.0.0.1%2Fmoodle%2Fmod%2Flti%2Fservice.php &oauth_body_hash%3DYLigJE%252B8wr7rCwOITqdc1IP3zFs%253D%26oauth_callback%3Dabout%253Ablank %26oauth_consumer_key%3Dkey%26oauth_nonce%3D6de4380ce2ab4d9a90e3fe1723dc5141 %26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1525940816%26oauth_version%3D1.0

  1. Body of the response:

    <?xml version="1.0" encoding="UTF-8"?>
    <imsx_POXEnvelopeResponse xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
        <imsx_POXHeader>
            <imsx_POXResponseHeaderInfo>
                <imsx_version>V1.0</imsx_version>
                <imsx_messageIdentifier>1688463600</imsx_messageIdentifier>
                <imsx_statusInfo>
                    <imsx_codeMajor>failure</imsx_codeMajor>
                    <imsx_severity>status</verity>
                    <imsx_description>Message signature not valid</imsx_description>
                    <imsx_messageRefIdentifier/>
                    <imsx_operationRefIdentifier>unknownRequest</imsx_operationRefIdentifier>
                </imsx_statusInfo>
            </imsx_POXResponseHeaderInfo>
        </imsx_POXHeader>
        <imsx_POXBody>
            <unknownResponse/>
        </imsx_POXBody>
    </imsx_POXEnvelopeResponse>
    

As you can see, both Base Strings are quite similar, and I can only find differences in the parts that are supposed to be different (e.g. timestamp or nonce), so I can't figure out why some of the request are being rejected while some others get accepted.

Any ideas on the reason this could be happening? Any suggestions on how to find out?

Thank you in advance for your help.

EDIT: I am adding here the code I use in the signing process

private IEnumerator SendGradeRequest(XmlDocument xml)
{
    string url = parametrosIniciales["lis_outcome_service_url"];

    byte[] entityBody = Encoding.UTF8.GetBytes(xml.OuterXml); 
    string bodyHash = oAuth.GetBodyHash(entityBody);
    Dictionary<string, string> oAuthParameters = oAuth.PrepareOAuthParameters(oAuth.GetSessionOAuthParameters(parametrosIniciales));
    oAuthParameters.Add("oauth_body_hash", bodyHash);

    Dictionary<string, string> headers = new Dictionary<string, string>();
    headers.Add("Content-Type", "application/xml");
    headers.Add("Authorization", oAuth.GetAuthorizationHeader(oAuthParameters));

    string signature = oAuth.GetRequestSignature(url, headers, entityBody); 
    oAuthParameters.Add("oauth_signature", signature);
    headers["Authorization"] = oAuth.GetAuthorizationHeader(oAuthParameters);

    WWW web = new WWW(url, entityBody, headers);

    // ... Code to manage the response
}

public string GetRequestSignature(string url, Dictionary<string, string> headers, byte[] body)
{
    string baseString = GetBaseString(url, headers, body);
    string key = GetKeyParts(JsonToDict(GameManager.Instance.parametrosIniciales));
    return ComputeHMACSHA1(key, baseString);
}

private string GetBaseString(string url, Dictionary<string, string> headers, object body = null)
{
    string[] parts = {  UrlEncodeRFC3986 (GetNormalizedHttpMethod(body)),
                        UrlEncodeRFC3986 (GetNormalizedHttpURL(url)),
                        UrlEncodeRFC3986 (GetSignableParameters(url, headers, body)) };
    return String.Join("&", parts);
}

private string GetKeyParts(Dictionary<string, string> oAuthParameters)
{
    string consumer, token, consumerSecret, tokenSecret;

    if (oAuthParameters.TryGetValue("oauth_consumer_key", out consumer))
        consumerSecret = UrlEncodeRFC3986(secrets[consumer]);
    else
        throw new Exception("Not consumer key found. If not using any encription, this method should not be called");

    if (oAuthParameters.TryGetValue("oauth_token", out token))
        tokenSecret = UrlEncodeRFC3986(secrets[token]);
    else
        tokenSecret = "";

    return consumerSecret + "&" + tokenSecret;
}

private string GetSignableParameters(string url, Dictionary<string, string> headers, object body = null)
{
    List<String> parametros = new List<string>();

    // 1. Parameters in the OAuth HTTP Authorization header excluding the realm parameter
    string oAuthHeader;
    if (headers.TryGetValue("Authorization", out oAuthHeader))
    {
        oAuthHeader = oAuthHeader.Substring(oAuthHeader.IndexOf(" ") + 1).Replace("\"", "");
        string[] pairs = oAuthHeader.Split(',');

        for (int i = 0; i < pairs.Length; i++)
        {
            string pair = pairs[i];
            string[] temp = pair.Split(new char[] { '=' }, 2);
            temp = UrlEncodeRFC3986(temp);
            pairs[i] = String.Join("=", temp);
        }
        parametros.InsertRange(parametros.Count, pairs);
    }

    // 2. HTTP GET parameters added to the URLs in the query part (as defined by [RFC3986] section 3)
    if (url.Contains("?"))
        parametros.InsertRange(parametros.Count, url.Substring(url.IndexOf("?")).Split('&'));

    // 3. Parameters in the HTTP POST request body (with a content-type of application/x-www-form-urlencoded)
    if (body != null && headers["Content-Type"] == "application/x-www-form-urlencoded" && body is Dictionary<string, string>)
    {
        Dictionary<string, string> cuerpo = RemoveSpareParameters((Dictionary<string, string>)body);
        parametros.InsertRange(parametros.Count, DictToPairsList(cuerpo, true));
    }

    // 4. Sort all parameters by lexicographical value
    parametros.Sort();

    // 5. Concatenate all parameters with '&'
    return String.Join("&", parametros.ToArray());
}
pgg66
  • 111
  • 4
  • According to the error message your signing is the issue. The code doing the signing might be helpful here to help find the problem. Signing LTI data can be tricky and it's best to use a tested library if possible – Michael Robellard May 14 '18 at 20:00
  • @MichaelRobellard Hello, Michael. I am aware the message points to my signature as the problem. However, I used a signature tool [link](http://lti.tools/oauth/) to check my process, feeding it with all the parameters I use and it returns the same Base Strings and signatures I get, both with my correct requests and those Moodle rejects. I have edited my question and added the code for most of my signing process, in case it might be of use. Thank you for your help. – pgg66 May 15 '18 at 14:51
  • I can't be 100% certain, but I think you are url encoding the XML body and it shouldn't be. "The oauth_body_hash [OBH, 11] is computed using a SHA-1 hash of the body contents and added to the Authorization header. All of the OAuth parameters, HTTP method, and URL are signed like any other OAuth signed request. Other than in producing the body hash value, the actual POST data is not involved in the computation of the oauth_signature." – Michael Robellard May 15 '18 at 21:20
  • Pretty sure its your signature, the body shouldn't be in the oauth_signature for an oauth_body_hash signed document, only in the oauth_body_hash and i don't think it should be urlencoded in that hash. – Michael Robellard May 15 '18 at 21:30
  • @MichaelRobellard thanks for your attention. I have double-checked and I think I do not include the proper body inside the signature process, but only the body hash I calculate. I might be mistaken, though; where in my code you think I do that? I may have confused you by the omission of some part. In any case, my point here is that my requests are apparently similar, and generated through the same process. However, Moodle keeps rejecting just some of them. That is what led me to believe it was an issue with Moodle. But I do not know how to troubleshoot this. – pgg66 May 16 '18 at 10:25

0 Answers0