What is the proper format for making an AJAX PUT request?
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?
Answers
-
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:
This is using the fetch method but you might able to do something similar using jQuery.
Hope that helps,
Dave
-
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.
-
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.
-
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)?
-
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.
-
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);
});
-
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.
-
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?
-
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?
-
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.
-
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.
-
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.
-
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.
-
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!