2

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'].

Def_Os
  • 5,301
  • 5
  • 34
  • 63
  • I think that I heard that ember and DRF are not compatible by default. Are you using something like https://github.com/dustinfarris/ember-django-adapter ? – ilse2005 Mar 09 '16 at 15:33
  • @ilse2005, I'm using `django-rest-framework-json-api` to make DRF responses JSON API spec compatible. – Def_Os Mar 11 '16 at 05:47

3 Answers3

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

Pavol
  • 1,200
  • 8
  • 20
1

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.

Def_Os
  • 5,301
  • 5
  • 34
  • 63
  • 3
    FWI, I was able to get both JSON API and normal JSON requests working (and hence authentication) by just making sure both `rest_framework.parsers.JSONParser` and `rest_framework_json_api.parsers.JSONParser` were included in the `DEFAULT_PARSER_CLASSES` for DRF's settings as well as `rest_framework_json_api.renderers.JSONRenderer` and `rest_framework.renderers.JSONRenderer` in the `DEFAULT_RENDERER_CLASSES` setting – Timmy O'Mahony Feb 23 '17 at 17:14
  • @TimmyO'Mahony, that also worked for me. Thanks, it made my code a lot more concise. – Def_Os May 30 '17 at 18:18
0

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?

Pavol
  • 1,200
  • 8
  • 20
  • Can you confirm at least that your request is having `application/vnd.api+json` instead of `application/json` in headers' `content-type` field? – Pavol Mar 11 '16 at 07:22
  • It does not. If it would, that probably solve my problem. – Def_Os Mar 11 '16 at 18:03
  • What I suggest is to make a breakpoint in `ajax()` function above and clarify program execution reaches the breakpoint. Afterwards I would probably step over to this.super() and observe where exactly gets `Content-Type` header rewritten – Pavol Mar 11 '16 at 18:59
  • you are right, it would reach this point only for Ember-Data requests, not for your authentication request. Please take a look at my post below, it should solve your problem now ;) – Pavol Mar 13 '16 at 07:38