I'm using ember-simple-auth and ember-simple-auth-token for allowing users to log into my app. However, when I call Django Rest Framework in the backend with a POST request to authenticate using a username and password, I get a 406 (Not Acceptable) error. This does not happen in the DRF browsable API, so the backend seems to work fine.
I suspect something related to CORS. I use django-cors-headers in Django, and allow all in my dev environment. I also use django-rest-framework-jwt and django-rest-framework-json-api packages, if that matters.
My API shows an OPTIONS and then a POST call being made:
[09/Mar/2016 07:15:54] "OPTIONS /api-token-auth/ HTTP/1.1" 200 0 [09/Mar/2016 07:15:54] "POST /api-token-auth/ HTTP/1.1" 406 114 Response headers:
HTTP/1.0 406 Not Acceptable Date: Wed, 09 Mar 2016 07:15:54 GMT Server: WSGIServer/0.2 CPython/3.5.1 X-Frame-Options: SAMEORIGIN Access-Control-Allow-Origin: * Content-Type: application/vnd.api+json Allow: POST, OPTIONS Vary: Accept Request headers:
POST /api-token-auth/ HTTP/1.1 Host: localhost:8000 Connection: keep-alive Content-Length: 2 Accept: application/json, text/javascript Origin: http://localhost:4200 User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/48.0.2564.116 Chrome/48.0.2564.116 Safari/537.36 Content-Type: application/json Referer: http://localhost:4200/login Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.8 The request headers do not show application/vnd.api+json but application/json instead. Unfortunately, no matter what I do in Ember is able to resolve that. I've unsuccessfully tried setting the headers to "Accept": "application/vnd.api+json" for my app's JSONAPIAdapter, and in ENV['ember-simple-auth-token'].
3 Answers
Answers 1
Implement your own authenticator which sets up headers used during authentication request:
// your-app/authenticators/your-custom-authenticator.js import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant'; export default OAuth2PasswordGrant.extend({ /** * your backend authentication endpoint * @overrides */ serverTokenEndpoint: `https://your.authentication.endpoint.sth/login`, /** * Makes a request to the authentication server. * This is what you need to override to customize your headers * set up for purposes of authentication. * @overrides */ makeRequest(url, data) { const options = { url: url, data: data, type: 'GET', dataType: 'json', accept: 'application/vnd.api+json', headers: { "Content-Type": 'application/vnd.api+json' } }; return Ember.$.ajax(options); } }); Refer to this custom authenticator in your (login) route/controller/wherever you need:
this.get('session').authenticate('authenticator:yourCustomAuthenticator', username, password).then(() => { // success, redirect, as you like.. }) Take a look at the Authenticators sections of ember-simple-auth docs to choose a parent authenticator as close to your needs as you need: ember-simple-auth - Authenticators
Answers 2
You should be able to explicitly set content-type in your adapter:
export default DS.JSONAPIAdapter.extend({ // set content-type upon every ajax request ajax: function(url, type, hash){ hash = hash || {} ; hash.headers = hash.headers || {}; hash.headers['Content-Type'] = 'application/vnd.api+json'; return this._super(url, type, hash); } }); Does it solve your problem?
Answers 3
I managed to solve this, more or less. It is an unfortunate combination of packages that led to some problems with having JSON API spec between Ember and DRF.
First, the overwriting of headers I managed to do in my controllers/login.js by simply adding the headers as an argument to .authenticate. Any args get passed to the ember-simple-auth authenticator. (I did not need to implement my own authenticator, as Pavol suggested in his answer.)
// controllers/login.js import Ember from 'ember'; export default Ember.Controller.extend({ session: Ember.inject.service('session'), actions: { authenticate: function() { var credentials = this.getProperties('identification', 'password'), authenticator = 'authenticator:jwt', // Set headers to accept JSON API format headers = { 'Accept': 'application/vnd.api+json', 'Content-Type': 'application/vnd.api+json' }; this.get('session').authenticate(authenticator, credentials, headers); } } }); This introduced the next problem: my content type was not actually JSON API spec, so I did need to implement my own authenticator to translate ember-simple-auth-token's JWT authenticator to produce JSON API spec compatible format. Didn't get it to work, but something like this:
// authenticators/jwt.js import Base from 'ember-simple-auth-token/authenticators/token'; export default Base.extend({ /** Returns an object used to be sent for authentication. @method getAuthenticateData @return {object} An object with properties for authentication. */ // Make sure this is JSON API compatible format. getAuthenticateData(credentials) { const authentication = { // This is apparently not valid JSON API spec, but you get the gist... 'data': [{ [this.identificationField]: credentials.identification, [this.passwordField]: credentials.password }] }; return authentication; } }); Now, on the backend, rest_framework_jwt and rest_framework_json_api were still not playing well together.
At this point, I decided that it was just a lot simpler to drop the need for JSON API spec on the auth endpoints: Ember's packages did not produce it, and DRF was having trouble parsing it!
So, I reverted everything on the Ember side, having it produce the request according to my original question. On the DRF side, I subclassed rest_framework_jwt's views and set the parser to DRF's default JSONParser:
""" Make the JWT Views ignore JSON API package and use standard JSON. """ from rest_framework_jwt.views import ObtainJSONWebToken, RefreshJSONWebToken, \ VerifyJSONWebToken from rest_framework.parsers import JSONParser from rest_framework.renderers import JSONRenderer class ObtainJSONWebTokenPlainJSON(ObtainJSONWebToken): parser_classes = (JSONParser, ) renderer_classes = (JSONRenderer, ) class RefreshJSONWebTokenPlainJSON(RefreshJSONWebToken): parser_classes = (JSONParser, ) renderer_classes = (JSONRenderer,) class VerifyJSONWebTokenPlainJSON(VerifyJSONWebToken): parser_classes = (JSONParser, ) renderer_classes = (JSONRenderer,) Final result: solved by having my API follow JSON API spec everywhere except the token authentication endpoints.
0 comments:
Post a Comment