What is the proper format for making an AJAX PUT request?

Options
Micah.Snabl8090
Micah.Snabl8090 Posts: 9 🌱
edited November 2022 in Development

I am trying to make an API call to set a specific Grade Value for a Learner. Here is the ajax call I am making to try and accomplish this.

 

$.ajax({

      url: '/d2l/api/le/1.26/6711/grades/319/values/184',

      type: 'PUT',

      data: {

        "Comments": { "Content": "", "Type": "Text" },

        "PrivateComments": { "Content": "", "Type": "Text" },

        "GradeObjectType": 3,

        "Value": "Late"

      }

    });

 

The response from this call is always a 403 Forbidden Error. The message that comes with this error is 'Forbidden'. One question within the community focused on permissions issues being the culprit. I do not believe that is the case here. I am accessing the site as a Super Administrator. When I impersonate a Learner on this same page, I instead get a 403 Forbidden - Not Authorized message.

 

The other question I saw in the community mentioned that you must include the correct values in the data being sent. As you can see, I have emulated the structure of that question and the API Documentation, including the Comments and PrivateComments along with the GradeObjectType and Value.

 

I have tried making the call with quotes around the parameter names and without quotes. I have tried encapsulating it all in a GradeValue object, and without. I have tried with JSON.stringify and without, and every combination of the above things I could imagine. Still, I get a 403 Forbidden - Forbidden message.

 

I even took a detour. I successfully used GET to acquire the users ActivationData, then tried to PUT the exact same data back. The GET was successful, and the PUT gave 403 Forbidden - Forbidden.

 

$.ajax({

      url: LPBaseRoute + '/users/' + 377 + '/activation',

      type: 'GET'

    }).done(function (returnedValue) {

      $.ajax({

        url: LPBaseRoute + '/users/' + 377 + '/activation',

        type: 'PUT',

        data: returnedValue

      });

    });

 

What am I doing wrong?

Tagged:

Answers

  • David Wag207
    David Wag207 Posts: 17
    edited November 2022
    Options

    Hey @Micah Snabl​,

     

    Are you attempting to do this action within a widget, or are you hosting your application outside of Brightspace and authenticating using Valence Auth or OAuth 2? If it's inside a widget then maybe this post will help you out:

     

    https://community.brightspace.com/s/question/0D56100001L9fLOCAZ/accessing-valence-api-data-from-a-custom-homepage-widget?s1oid=00D6100000080tO&OpenCommentForEdit=1&s1nid=0DB6100000001Aq&emkind=chatterPostNotification&s1uid=00561000002ie2j&emtm=1513650322952&fromEmail=1&s1ext=0

     

    This is using the fetch method but you might able to do something similar using jQuery.

     

    Hope that helps,

    Dave

     

     

  • Micah.Snabl8090
    Micah.Snabl8090 Posts: 9 🌱
    edited November 2022
    Options

    I am doing this within a widget on the homepage. As such, I have not done any additional authentication steps.

     

    I have added an ajax setting

    xhrFields: {

    withCredentials: true

    }

    in an attempt to emulate the referenced discussion's fetch setting. It has not produced any change. I see that the Response header has

    Access-Control-Allow-Credentials: false

     

    As noted above, all of my various GET requests are working fine, returning the proper data. Only PUT requests are failing.

  • Micah.Snabl8090
    Micah.Snabl8090 Posts: 9 🌱
    edited November 2022
    Options

    Addendum: I still get the 403: Forbidden "Forbidden" message, even when the Grade Item has been deleted. I would take that to mean that the PUT itself is being rejected, before any data validation can be done.

  • Viktor.H.147
    Viktor.H.147 Posts: 41
    edited November 2022
    Options

    Against a test instance, I've confirmed that the API route works, when used from a separate host (with the OAuth2-style authentication), and PUTting request body data in the general form you've provided:

     

    >>>

     

    ## previous set up values for url, incoming_grade_value, user_context object for auth signing

    ## using the requests python library to make requests

     

    In [1]: url

    Out[1]: 'https://testhost.bspc.com/d2l/api/le/1.26/129468/grades/7461/values/15679'

     

    In [2]: incoming_grade_value

    Out[2]: 

    {'Comments': {'Content': '', 'Type': 'Text'},

     'GradeObjectType': 3,

     'PrivateComments': {'Content': '', 'Type': 'Text'},

     'Value': 'F'}

     

    In [3]: r = requests.put(url, json=incoming_grade_value, auth=user_context)

     

    In [4]: r.status_code

    Out[104]: 200

     

    In [5]: r.content

    Out[5]: b''

     

    In [6]: r.request.body

    Out[6]: b'{"Value": "F", "GradeObjectType": 3, "Comments": {"Content": "", "Type": "Text"}, "PrivateComments": {"Content": "", "Type": "Text"}}'

     

    In [7]: r.request.headers

    Out[7]: {'User-Agent': 'python-requests/2.18.4', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '133', 'Content-Type': 'application/json', 'Authorization': 'Bearer <...tokendata...>'}

     

    In [8]: r.request.method

    Out[8]: 'PUT'

    <<<

     

    Based on your comments, it sounds to me like there's some kind of finickiness making PUT requests in the manner you're doing it... have you been able to do POSTs (i.e. "creating a user" or something similar)?

  • Micah.Snabl8090
    Micah.Snabl8090 Posts: 9 🌱
    edited November 2022
    Options

    After reading your post, I went in and attempted a POST command in the same way. It returns the same Forbidden message as the PUT attempt.

     

    We have another website where we handle user registrations, and the POST requests from that externally authorized site work correctly. It is only within D2L using the automatic authorization that this fails.

     

    Perhaps I need to include the special authorization steps, even though I am operating within Brightspace? This seems odd, since all the GET requests succeed with no extra effort.

  • Steve.B.446
    Steve.B.446 Posts: 75
    edited November 2022
    Options

    To do anything with a write-access (eg PUT or POST) request in a widget, you seem to need to get the logged in user's token and send it with the request.

     

    You can get the token from a GET request to /d2l/lp/auth/xsrf-tokens

     

    then send the referrerToken that you get back in your AJAX request, so with jquery it's something like

     

    $.ajax({

     method: "PUT",

      beforeSend: function(request) {

      request.setRequestHeader("X-Csrf-Token", token.referrerToken);

     },

     url: someurl,

     data: somedata,

    })

     .success(function( msg ) {

    doSomethingWith(msg);

     });

     

  • Micah.Snabl8090
    Micah.Snabl8090 Posts: 9 🌱
    edited November 2022
    Options

    Thanks for that input, @Steve Bentley​ . I tried something similar using the XSRF.Token in Local Storage. That changed my errors to 400's instead of 403's. You mentioned acquiring the token from an api call. I was not able to discover any documentation on that call! Would it return a different token value than the one in my Local Storage? If so, could you point me in the right direction on how to complete that call?

  • Viktor.H.147
    Viktor.H.147 Posts: 41
    edited November 2022
    Options

    The API call that Steve refers to is an undocumented API that is not officially supported and may not exist in the future. Currently, it requests an XSRF token which the caller must provide when making non-GET requests from the user agent when relying on the browser session as the authentication mechanism for the API call.

  • Steve.B.446
    Steve.B.446 Posts: 75
    edited November 2022
    Options

    To be perfectly honest, I cribbed this from somebody else's code sample, so I don't pretend to fully understand it, but if you're getting 400s that means the data string you are sending is probably incomplete, the API test tool can be very handy for troubleshooting that.

     

    You'll want the token for the logged in user.  ​If you do a GET request for the URL I mentioned you will get back a JSON response 

     

    {"hitCodePrefix":"xxxx","referrerToken":"yyyy"}

     

    and it's the yyy value that you need to pass in your POST query.

     

    Viktor - thanks for the heads up that it's unsupported, is there a recommended way to do this?

     

  • Viktor.H.147
    Viktor.H.147 Posts: 41
    edited November 2022
    Options

    The currently supported way for third-party clients to have access to Brightspace APIs is to use one of the three-legged ID-Key or OAuth authentication code grant workflows from a back-end web service application that's registered through Brightspace's Manage Extensibility feature and manages the secrets safe from the user agent.

     

    We do currently employ this undocumented method of requesting an XSRF token so that a client application using the browser session as auth can make non-GET calls, but it's not a recommended approach for third-parties.

  • Steve.B.446
    Steve.B.446 Posts: 75
    edited November 2022
    Options

    Hi Viktor

     

    This is for use from within a widget, which I was under the impression should run from the current user's context anyway without having to do this kind of work around?  It seems fine for GET requests but fails for PUT and POST.

  • Viktor.H.147
    Viktor.H.147 Posts: 41
    edited November 2022
    Options

    Yes; my original responses were not quite accurate (I've since edited them) -- this unsupported API merely allows non-GET calls to be used in the circumstances you describe. Third-party integrations that want to have more than just read-only API access are officially supported via the authentication workflows in the API reference documentation.

  • Micah.Snabl8090
    Micah.Snabl8090 Posts: 9 🌱
    edited November 2022
    Options

    I have confirmed that the XSRF token from the Local Storage is identical to the one from the API call. I have tried adding the header in the manner Steve described, as well as manually setting the header

     

    headers: {

    'X-Csrf-Token': XSRFString

    }

     

    Neither has produced any results. I went in to the API Test Tool and tried the request, passing the exact same formatted data as my calls, and it was successful.

  • Micah.Snabl8090
    Micah.Snabl8090 Posts: 9 🌱
    edited November 2022
    Options

    With the help of D2L support, we have arrived at a functioning API call.

    $.ajax({

            url: '/d2l/api/le/1.26/6711/grades/319/values/184',

            type: 'PUT',

            data: JSON.stringify({

              "Comments": { "Content": "", "Type": "Text" },

              "PrivateComments": { "Content": "", "Type": "Text" },

              "GradeObjectType": 3,

              "Value": "Late"

            }),

            headers: {

              'X-CSRF-TOKEN': localStorage.getItem("XSRF.Token")

            }

          });

     

    We were pointed to the fact that this API call requires a JSON parameter, yet our data was not correctly formatted as JSON. JSON.stringify solves this issue. The X-Csrf-Token is also required. We found locally that the result from that API call mentioned above that is identical to the localStorage item, when you're making the call from within the Brightspace environment.

     

    It feels good to have this operational!