André Fratelli
André Fratelli

Reputation: 6068

Authenticating with LinkedIn using django-all-access

I'm using django-all-access to implement OAuth authentication for Facebook, Twitter, and LinkedIn. Facebook and Twitter are working fine, LinkedIn is redirecting me to the wrong page.

Here's my setup (consumer keys and secrets are obviously obfuscated):

[
    {
        "pk": null,
        "model": "allaccess.provider",
        "fields": {
            "name": "facebook",
            "consumer_key": "xxx",
            "consumer_secret": "xxx",
            "authorization_url": "https://www.facebook.com/dialog/oauth",
            "access_token_url": "https://graph.facebook.com/oauth/access_token",
            "request_token_url": "",
            "profile_url": "https://graph.facebook.com/me"
        }
    },
    {
        "pk": null,
        "model": "allaccess.provider",
        "fields": {
            "name": "twitter",
            "consumer_key": "xxx",
            "consumer_secret": "xxx",
            "authorization_url": "https://api.twitter.com/oauth/authenticate",
            "access_token_url": "https://api.twitter.com/oauth/access_token",
            "request_token_url": "https://api.twitter.com/oauth/request_token",
            "profile_url": "https://api.twitter.com/1.1/account/verify_credentials.json"
        }
    },
    {
        "pk": null,
        "model": "allaccess.provider",
        "fields": {
            "name": "linkedin",
            "consumer_key": "xxx",
            "consumer_secret": "xxx",
            "authorization_url": "https://www.linkedin.com/uas/oauth2/authorization",
            "access_token_url": "https://www.linkedin.com/uas/oauth2/accessToken",
            "request_token_url": "",
            "profile_url": "https://api.linkedin.com/v1/people/~"
        }
    }
]

Both Facebook and Twitter are using the correct authentication flow and registering users properly, but Twitter redirects me to the wrong page and is not registering users at all. Here's the LinkedIn flow (I removed most parameters, and left the redirect_uri):

  1. https://www.linkedin.com/uas/oauth2/authorization?redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Faccounts%2Fcallback%2Flinkedin%2F&response_type=code
  2. http://localhost:8000/accounts/callback/linkedin/
  3. http://localhost:8000/accounts/login/

My first guess would be that my app settings are improperly configured in LinkedIn, so here are my settings:

OAuth 2.0 Redirect URLs: http://localhost:8000/accounts/callback/linkedin/,http://localhost:8000/accounts/profile/

OAuth 1.0 Accept Redirect URL: http://localhost:8000/accounts/profile/

My second guess would be that the profile_url parameter is wrong, which is https://api.linkedin.com/v1/people/~.

Can anybody help? Best.

Upvotes: 1

Views: 728

Answers (1)

André Fratelli
André Fratelli

Reputation: 6068

There were two things wrong with this. First, LinkedIn expects the access_token parameter to be named oauth2_access_token, which is not compliant with the RFC 6750. Also, LinkedIn does not return JSON by default, which is expected by the allaccess clients. As such, you'd also need to add format=json as a parameter in the call.

This can be achieved mostly by customising the OAuth2Client.request method, but in my case I went a little further. The allaccess framework sends the access token as a query parameter, which is usually discouraged because the tokens are then logged on the server, which might not be safe. Instead, both OAuth 1 and 2 support sending the token in the Authorization request header. OAuth 1 is a bit more complicated, while OAuth 2 requires only a bearer token.

As such, I customised the OAuth2Client class to handle both these situations.

from allaccess.clients import OAuth2Client as _OAuth2Client
from requests.api import request

class OAuth2Client(_OAuth2Client):
    
    def request(self, method, url, **kwargs):
        
        user_token = kwargs.pop('token', self.token)
        token, _ = self.parse_raw_token(user_token)
        
        if token is not None:
            
            # Replace the parent method so the token is sent on the headers. This is
            # safer than using query parameters, which is what allaccess does
            headers = kwargs.get('headers', {})
            headers['Authorization'] = self.get_authorization_header(token)
            kwargs['headers'] = headers
            
        return request(method, url, **kwargs)
    
    def get_authorization_header(self, token):
        return 'Bearer %s' % (token,)
    
class OAuth2LinkedInClient(OAuth2Client):
    
    def request(self, method, url, **kwargs):
        
        # LinkedIn does not return JSON by default
        params = kwargs.get('params', {})
        params['format'] = 'json'
        kwargs['params'] = params
        
        return super(OAuth2LinkedInClient, self).request(method, url, **kwargs)
        

OAuth2Client now sends the access token in the request headers instead of the query parameters. Also, the LinkedIn client adds the format query parameter and sets it to json. There's no need to replace OAuth 1 authentication as it already sends the token in the headers.

Unfortunately, that's not the whole deal. We now need to let allaccess know to use these clients, and we do that by customising the views. Here's my implementation:

from allaccess.views import OAuthRedirect as _OAuthRedirect
from allaccess.views import OAuthCallback as _OAuthCallback
from allaccess.views import OAuthClientMixin as _OAuthClientMixin
from django.core.urlresolvers import reverse
from authy.clients import OAuth2Client, OAuth2LinkedInClient

class OAuthClientMixin(_OAuthClientMixin):
    
    def get_client(self, provider):
        
        # LinkedIn is... Special
        if provider.name == 'linkedin':
            return OAuth2LinkedInClient(provider)
        
        # OAuth 2.0 providers
        if not provider.request_token_url:
            return OAuth2Client(provider)
        
        # Let allaccess chose other providers (those will be mostly OAuth 1)
        return super(OAuthClientMixin, self).get_client(provider)
        
class OAuthRedirect(OAuthClientMixin, _OAuthRedirect):
    
    # This is necessary because we'll be setting these on our URLs, we can no longer
    # use allaccess' URLs.
    def get_callback_url(self, provider):
        return reverse('authy-callback', kwargs={ 'provider': provider.name })
    
class OAuthCallback(OAuthClientMixin, _OAuthCallback):
    
    # We need this. Notice that it inherits from our own client mixin
    pass

Now set the URLs to map to our own implementation:

from django.conf.urls import url
from .views import OAuthRedirect, OAuthCallback

urlpatterns = [
    url(r'^login/(?P<provider>(\w|-)+)/$', OAuthRedirect.as_view(), name='authy-login'),
    url(r'^callback/(?P<provider>(\w|-)+)/$', OAuthCallback.as_view(), name='authy-callback'),
]

There's one other issue left unsolved, however. The problem is that other classes also use clients. I could find the allaccess.models.AccountAccess.api_client method, for instance. I'm not sure if there are more. Now the problem is that our views might be using our clients, while other classes are using different clients. I'm not sure to what extend this could be a problem, but so for it has not bitten me and for now I'm proceeding with this code.

Finally, I would like to credit and thank Mark Lavin, the creator of the allaccess framework. I contacted him and it was his guidance which lead me to these conclusions.

Hope this helps someone else as well! Farewell.

Upvotes: 3

Related Questions