I am trying to implement a web service with filtered resources access (OAuth authentication) with Django and I had a couple of questions.
I created two webservers:
http://localhost:8080 : Web Service Provider (using django-piston for the webservice)
http://localhost:8000 : Web Service Consumer
I am trying to use the version 1.0a of OAuth to authenticate the consumer against the provider. The workflow of this protocol is described here.
In a nutshell, here are the different steps (name of the resource exchanged):
The consumer requests a token from the provider (key, secret)
If the consumer is valid, the provider returns it a token (oauth_token, oauth_token_secret)
The consumer redirects the User to the provider to login/grant access (oauth_token)
The user grant access to the consumer for the resource.
The provider gives the consumer a token verifier (token_verifier)
The consumer requests an access_token (key, secret, oauth_token, oauth_token_secret, oauth_verifier)
The provider gives the consumer an access_token (oauth_token)
The consumer uses its oauth_token to access the resource
Here is the code of the views of my consumer:
from django.shortcuts import render_to_response
from django.http import HttpResponse, HttpResponseRedirect
import oauth2 as oauth
import urlparse
REQUEST_TOKEN_URL = 'http://127.0.0.1:8080/api/authentication/request_token/'
AUTHORIZATION_URL = 'http://127.0.0.1:8080/api/authentication/authorize/'
ACCESS_TOKEN_URL = 'http://127.0.0.1:8080/api/authentication/access_token/'
CONSUMER_CALLBACK_URL = 'http://127.0.0.1:8000/request_access_token/'
CONSUMER_KEY = 'key'
CONSUMER_SECRET = 'secret'
consumer = oauth.Consumer(CONSUMER_KEY, CONSUMER_SECRET)
client = oauth.Client(consumer)
def request_token(request):
"""
Contacts the service provider to get a token.
"""
resp, content = client.request(REQUEST_TOKEN_URL, 'GET')
oauth_token = dict(urlparse.parse_qsl(content)).get('oauth_token', None)
oauth_token_secret = dict(urlparse.parse_qsl(content)).get('oauth_token_secret',
None)
if oauth_token is None:
return render_to_response('home.html', {'data': 'NO TOKEN FOUND'})
else:
request.session['oauth_token'] = oauth_token
request.session['oauth_token_secret'] = oauth_token_secret
return HttpResponseRedirect('request_user_permission/')
def request_user_permission(request):
"""
Redirects the user to the service provider to get permission if
token provided.
"""
oauth_token = request.session['oauth_token']
if oauth_token is None:
return render_to_response('home.html', {'data': 'NO TOKEN FOUND'})
else:
return HttpResponseRedirect("%s?oauth_token=%s&oauth_callback=%s"
% (AUTHORIZATION_URL, oauth_token, CONSUMER_CALLBACK_URL))
def request_access_token(request):
"""
Requests an access token from the service provider
if the user granted permission.
"""
error = request.GET.get('error', None)
if error is None:
oauth_verifier = request.GET.get('oauth_verifier', None)
if oauth_verifier is None:
return render_to_response('home.html',
{'data': 'UNKNOWN ERROR HAPPENED'})
else:
# User permission granted, requesting access token
oauth_token = request.session['oauth_token']
oauth_token_secret = request.session['oauth_token_secret']
token = oauth.Token(oauth_token, oauth_token_secret)
token.set_verifier(oauth_verifier)
client.token = token
resp, content = client.request(ACCESS_TOKEN_URL, 'POST')
access_token = dict(urlparse.parse_qsl(content))
return render_to_response('home.html', {'data': access_token})
else:
return render_to_response('home.html', {'data': error})
General OAuth Questions
How many token should the consumer have? One? One per User? One per Resource?
How is the consumer supposed to store its token(s)?
How do you specify which resources the consumer can access with its token? Shouldn't the consumer be able to provide the id of the resource it wants to get access to when redirecting the user to the service provider (step 3)?
If a consumer wants to access a resource for which the user has already granted the access in the past, should it redirect the user to the service provider anyway (and let the service provider return the oauth_verifier instantly, instead of asking the User for permission)?
Technical problems
Right now, I am using local memory cached sessions to store the token, but it doesn't work:
When the sessions are activated on the consumer server, the User has to login every time on the service provider server (even if he was already logged in).
In the first view (requesting the token) I store the oauth_token and oauth_token_secret in the request's session. When I try to access it in the second view (before redirecting the user), it works. But when I try to access it in the last view (after the redirection) it doesn't (KeyError, oauth_token is not found in the request.session dictionary)
Thank you!
I'm in the same-ish boat (implementing an API with OAuth authentication), and I came across the django-oauth-plus project in my research. The tutorial does a good job of walking through each step of the process outlined in the diagram you linked to. The code itself seems a pretty complete implementation of OAuth (not sure if I know what I'm talking about tho, just learning this stuff).
Also, this guy has a pretty great tutorial about the basics of OAuth (I struggled with that).
Related
I'm developing a Django (2.2.3) application with Django Microsoft Auth installed to handle SSO with Azure AD. I've been able to follow the quickstart documentation to allow me to log into the Django Admin panel by either using my Microsoft identity, or a standard username and password I've added to the Django user table. This all works out of the box and is fine.
My question put (really) simply is "What do I do next?". From a user's perspective, I'd like them to:
Navigate to my application (example.com/ or example.com/content) - Django will realise they aren't authenticated, and either
automatically redirect them to the SSO portal in the same window, or
redirect them to example.com/login, which requires them to click a button that will open the SSO
portal in a window (which is what happens in the default admin case)
Allow them to sign in and use MFA with their Microsoft Account
Once successful redirect them to my #login_required pages (example.com/content)
Currently, at the root of my navigation (example.com/), I have this:
def index(request):
if request.user.is_authenticated:
return redirect("/content")
else:
return redirect("/login")
My original idea was to simply change the redirect("/login") to redirect(authorization_url) - and this is where my problems start..
As far as I can tell, there isn't any way to get the current instance(?) of the context processor or backend of the microsoft_auth plugin to call the authorization_url() function and redirect the user from views.py.
Ok... Then I thought I'd just instantiate the MicrosoftClient class that generates the auth URL. This didn't work - not 100% sure why, but it think it may have something to do with the fact that some state variable used by the actual MicrosoftClient instance on the backend/context processor is inconsistent with my instance.
Finally, I tried to mimic what the automatic /admin page does - present an SSO button for the user to click, and open the Azure portal in a separate window. After digging around a bit, I realise that I fundamentally have the same problem - the auth URL is passed into the admin login page template as inline JS, which is later used to create the Azure window asynchronously on the client side.
As a sanity check, I tried to manually navigate to the auth URL as it is presented in the admin login page, and that did work (though the redirect to /content didn't).
At this point, given how difficult I think I'm making it for myself, I'm feel like I'm going about this whole thing completely the wrong way. Sadly, I can't find any documentation on how to complete this part of the process.
So, what am I doing wrong?!
A couple more days at this and I eventually worked out the issues myself, and learned a little more about how Django works too.
The link I was missing was how/where context processors from (third party) Django modules pass their context's through to the page that's eventually rendered. I didn't realise that variables from the microsoft_auth package (such as the authorisation_url used in its template) were accessible to me in any of my templates by default as well. Knowing this, I was able to implement a slightly simpler version of the same JS based login process that the admin panel uses.
Assuming that anyone reading this in the future is going through the same (learning) process I have (with this package in particular), I might be able to guess at the next couple of questions you'll have...
The first one was "I've logged in successfully...how do I do anything on behalf of the user?!". One would assume you'd be given the user's access token to use for future requests, but at the time of writing this package didn't seem to do it in any obvious way by default. The docs for the package only get you as far as logging into the admin panel.
The (in my opinion, not so obvious) answer is that you have to set MICROSOFT_AUTH_AUTHENTICATE_HOOK to a function that can be called on a successful authentication. It will be passed the logged in user (model) and their token JSON object for you to do with as you wish. After some deliberation, I opted to extend my user model using AbstractUser and just keep each user's token with their other data.
models.py
class User(AbstractUser):
access_token = models.CharField(max_length=2048, blank=True, null=True)
id_token = models.CharField(max_length=2048, blank=True, null=True)
token_expires = models.DateTimeField(blank=True, null=True)
aad.py
from datetime import datetime
from django.utils.timezone import make_aware
def store_token(user, token):
user.access_token = token["access_token"]
user.id_token = token["id_token"]
user.token_expires = make_aware(datetime.fromtimestamp(token["expires_at"]))
user.save()
settings.py
MICROSOFT_AUTH_EXTRA_SCOPES = "User.Read"
MICROSOFT_AUTH_AUTHENTICATE_HOOK = "django_app.aad.store_token"
Note the MICROSOFT_AUTH_EXTRA_SCOPES setting, which might be your second/side question - The default scopes set in the package as SCOPE_MICROSOFT = ["openid", "email", "profile"], and how to add more isn't made obvious. I needed to add User.Read at the very least. Keep in mind that the setting expects a string of space separated scopes, not a list.
Once you have the access token, you're free to make requests to the Microsoft Graph API. Their Graph Explorer is extremely useful in helping out with this.
So I made this custom view in Django based on https://github.com/Azure-Samples/ms-identity-python-webapp.
Hopefully, this will help someone.
import logging
import uuid
from os import getenv
import msal
import requests
from django.http import JsonResponse
from django.shortcuts import redirect, render
from rest_framework.generics import ListAPIView
logging.getLogger("msal").setLevel(logging.WARN)
# Application (client) ID of app registration
CLIENT_ID = "<appid of client registered in AD>"
TENANT_ID = "<tenantid of AD>"
CLIENT_SECRET = getenv("CLIENT_SECRET")
AUTHORITY = "https://login.microsoftonline.com/" + TENANT_ID
# This resource requires no admin consent
GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me'
SCOPE = ["User.Read"]
LOGIN_URI = "https://<your_domain>/login"
# This is registered as a redirect URI in app registrations in AD
REDIRECT_URI = "https://<your_domain>/authorize"
class Login(ListAPIView):
'''initial login
'''
def get(self, request):
session = request.session
id_token_claims = get_token_from_cache(session, SCOPE)
if id_token_claims:
access_token = id_token_claims.get("access_token")
if access_token:
graph_response = microsoft_graph_call(access_token)
if graph_response.get("error"):
resp = JsonResponse(graph_response, status=401)
else:
resp = render(request, 'API_AUTH.html', graph_response)
else:
session["state"] = str(uuid.uuid4())
auth_url = build_auth_url(scopes=SCOPE, state=session["state"])
resp = redirect(auth_url)
else:
session["state"] = str(uuid.uuid4())
auth_url = build_auth_url(scopes=SCOPE, state=session["state"])
resp = redirect(auth_url)
return resp
class Authorize(ListAPIView):
'''authorize after login
'''
def get(self, request):
session = request.session
# If states don't match login again
if request.GET.get('state') != session.get("state"):
return redirect(LOGIN_URI)
# Authentication/Authorization failure
if "error" in request.GET:
return JsonResponse({"error":request.GET.get("error")})
if request.GET.get('code'):
cache = load_cache(session)
result = build_msal_app(cache=cache).acquire_token_by_authorization_code(
request.GET['code'],
# Misspelled scope would cause an HTTP 400 error here
scopes=SCOPE,
redirect_uri=REDIRECT_URI
)
if "error" in result:
resp = JsonResponse({"error":request.GET.get("error")})
else:
access_token = result["access_token"]
session["user"] = result.get("id_token_claims")
save_cache(session, cache)
# Get user details using microsoft graph api call
graph_response = microsoft_graph_call(access_token)
resp = render(request, 'API_AUTH.html', graph_response)
else:
resp = JsonResponse({"login":"failed"}, status=401)
return resp
def load_cache(session):
'''loads from msal cache
'''
cache = msal.SerializableTokenCache()
if session.get("token_cache"):
cache.deserialize(session["token_cache"])
return cache
def save_cache(session,cache):
'''saves to msal cache
'''
if cache.has_state_changed:
session["token_cache"] = cache.serialize()
def build_msal_app(cache=None, authority=None):
'''builds msal cache
'''
return msal.ConfidentialClientApplication(
CLIENT_ID, authority=authority or AUTHORITY,
client_credential=CLIENT_SECRET, token_cache=cache)
def build_auth_url(authority=None, scopes=None, state=None):
'''builds auth url per tenantid
'''
return build_msal_app(authority=authority).get_authorization_request_url(
scopes or [],
state=state or str(uuid.uuid4()),
redirect_uri=REDIRECT_URI)
def get_token_from_cache(session, scope):
'''get accesstoken from cache
'''
# This web app maintains one cache per session
cache = load_cache(session)
cca = build_msal_app(cache=cache)
accounts = cca.get_accounts()
# So all account(s) belong to the current signed-in user
if accounts:
result = cca.acquire_token_silent(scope, account=accounts[0])
save_cache(session, cache)
return result
def microsoft_graph_call(access_token):
'''graph api to microsoft
'''
# Use token to call downstream service
graph_data = requests.get(
url=GRAPH_ENDPOINT,
headers={'Authorization': 'Bearer ' + access_token},
).json()
if "error" not in graph_data:
return {
"Login" : "success",
"UserId" : graph_data.get("id"),
"UserName" : graph_data.get("displayName"),
"AccessToken" : access_token
}
else:
return {"error" : graph_data}
I have written a simple Python Flask API which does operations like adding data to Database and getting data from Database, there is no UI for this API, Now I want to implement OAuth authentication system for this simple API, As there is NO GUI, I cant use google or FB Oauth Providers which redirects users to there login page.
In simple words, i want to create my own GUI less oauth Authentication system which secures my API as any user who wants to access my API should pass through this authentication system by passing access token in a header
I need Oauth Authentication system of my own for the API's below:
from flask import Flask, redirect, url_for, session
from flask import Flask,jsonify,request,make_response
from flask_login import login_user,logout_user,current_user,login_required,LoginManager,login_manager
from flask_oauth import OAuth
import json
from flask_mysqldb import MySQL
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_oauthlib.provider import OAuth1Provider
app = Flask(__name__)
class MYWIFI(db.Model):
__tablename__ = 'MYWIFI'
id = db.Column('id', db.Integer, primary_key=True)
data = db.Column('data', db.Unicode)
def __init__(self, id, data):
self.id = id
self.data = data
#app.route('/getall')
def getall():
access_token = get_access_token()
if access_token is None:
return redirect(url_for('login'))
else:
languages = [u.__dict__ for u in db.session.query(MYWIFI).all()]
for d in languages:
del d['_sa_instance_state']
print(languages)
languagesJSON = json.dumps(languages)
return languagesJSON
#app.route('/insert', methods=['GET','POST'])
def insert():
access_token = get_access_token()
if access_token is None:
return redirect(url_for('login'))
else:
if request.method == 'POST':
insert = request.get_json()
id = insert['id']
data = insert['data']
print id
print data
new = MYWIFI(id, data)
db.session.add(new)
db.session.commit()
return "Success"
def main():
app.run()
if __name__ == '__main__':
main()
Please can anyone help me in kick starting this
I appreciate for this help
If I understood correctly, what you want is to build API endpoints which are protected by OAuth 2.0 tokens. If that's the case you as the API builder does not have to worry how token obtaining process happens. The client that consumes your APIs must perform the token obtaining and pass them to your end.
About sending in headers, try to stick with standards already exist in the OAuth 2 domain. RFC6750 defines how to use tokens once a client obtains them. It defines bearer authentication schema to transmit access tokens. Check section 2.1 to how header is set.
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM
Where mF_9.B5f-4.1JqM is the access token. Once your API receives a request, from your end you must validate the access token before granting access. For this there is RFC7662 which define the methodology to validate access token against the authorization server. See section 2 Introspection endpoint to get an understanding of it. Alternatively, access token can come in a JWT format thus allowing it to be self contained.
Unfortunately, I do not have code for proposed solutions. They will considerable amount of codes. But I suggest you to separate authorization logic from your code. That means validation of authorization must be a separate module in your python code. But below I give a suggestion with my python knowledge.
#app.route('/insert', methods=['GET','POST'])
def insert():
access_token = get_access_token()
# Auth Validation for insert - This is based on access tokens
# If token is invalid/not-present, exception is thrown with HTTP 401 - unauthorized
my_auth_module.validate_access_token(access_token)
if request.method == 'POST':
insert = request.get_json()
id = insert['id']
Also one final thing, your API should not worry about redirection for login. Let it be handled by your API client upon the 401 - Unathorized response.
I am trying to get social authentication working for my mobile app (an Ionic app on Android). Django rest framework backend with rest_framework_jwt, social_django, and rest_social_auth.
On my Ionic app I was using satellizer.js, however, I can't use InAppBrowser so now I am trying to do the following with cordova-plugin-googleplus:
Step#1 (On client/app)
if (provider == 'google') {
// Use Google API Natively to get tokens and user info
window.plugins.googleplus.login(
{
// TODO Get the WebClient from App settings
'webClientId': '[*.myclientid]', // optional clientId of your Web application from Credentials settings of your project - On Android, this MUST be included to get an idToken. On iOS, it is not required.
'offline': true, // optional, but requires the webClientId - if set to true the plugin will also return a serverAuthCode, which can be used to grant offline access to a non-Google server
}) ................
Result: This gets me a valid response with both a idToken, serverAuthCode, and a userId.
Step#2
I am not sure what the next step is. Originally, I was going to try using Django rest_social_auth to do the following from my client/app:
POST /api/login/social/
with data (json)
provider=google&code=ASLKDJASLDKJASLD
Which was supposed to return a JWT token (from my understanding of the docs), however, it is not passing the JWTAuthMixin as there is no value returned from a call to get_authorization_header(request).split() in that Mixin. These means that nothing is returned to my client/app except a 400 error.
Am I supposed to be adding a header to my Ionic app POST when passing my idToken or serverAuthCode? Or am I on the wrong side of the tracks...
Are there any implementation recommendations for this auth flow?
So far I did the following and it works.
1. On app/client
(The client uses satellizer.js and the cordova-plugin-googleplus)
if (provider == 'google') {
// Use Google API Natively to get tokens and user info
window.plugins.googleplus.login(
{
// TODO Get the WebClient from App settings
'webClientId': '*[googleclientid]*.apps.googleusercontent.com',
'offline': true
},
function (obj) {
$http.post(SERVER.url + '[MY BACKEND URL]' + '/google-oauth2/', {code: obj.idToken, servAuthCode: obj.serverAuthCode})
.success(function(data){
$auth.setToken(data.jwt_token);
/.. Do something ../
})
.error(function(data){
console.log("There was an error" + JSON.stringify(data));
});
},
function (msg) {
// TODO Set Error states
console.error('error: ' + msg);
}
);
}
Summary
The app calls the Google plus API googleplus.login method (sending my webClientId)
I post the resulting idToken and serverAuthCode obtained from google after login to my Django backend.
2. My backend methods
URL
My app/client hits the url(r'^[MY BACKEND URL]/(?P<backend>[\w-]+)/$', ObtainAuthToken.as_view(), ),
View
This calls the following view and functions:
class ObtainAuthToken(APIView):
permission_classes = (AllowAny,)
def post(self, request, backend):
data = request.data
user_tokenID = data['code']
server_auth_code = data['servAuthCode']
if user_tokenID and server_auth_code and verify_google_user_token_ID(user_tokenID):
# Get Google OAuth credentials for the verified GOOGLE user.
credentials = settings.GOOGLE_FLOW.step2_exchange(server_auth_code)
# Here we call PSA to authenticate like we would if we used PSA on server side.
user = register_by_access_token(request, backend, token=credentials.access_token)
# If user is active we get or create the REST token and send it back with user data
if user and user.is_active:
# Generate JWT token for user and pass back to client
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return JsonResponse({'id': user.id, 'name': user.username, 'jwt_token': token})
return JsonResponse({'status':'false','error':'Bad Credentials, check the Access Token and/or the UID'},
status=403)
def verify_google_user_token_ID(user_tokenID):
try:
google_http_request = google.auth.transport.requests.Request()
idinfo = verify_token(user_tokenID, request=google_http_request,
audience=settings.SOCIAL_AUTH_GOOGLE_OAUTH2_FULL_KEY)
# Or, if multiple clients access the backend server:
if idinfo['aud'] not in [settings.GOOGLE_APP_ID_ANDROID, settings.GOOGLE_APP_ID_WEB]:
raise crypt.AppIdentityError("Unrecognized client.")
if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
raise crypt.AppIdentityError("Wrong issuer.")
return True
except crypt.AppIdentityError as e:
# Invalid token
return False
#psa('social:complete')
def register_by_access_token(request, backend, token):
backend = social_core.backends.google.GoogleOAuth2()
user = backend.do_auth(access_token=token, backend=backend)
if user:
return user
else:
return None
3. Back on the client
My client then looks at the response and takes the returned JWT and loads it to memory with $auth.setToken(data.jwt_token);
I think this works for now, but I still have to deal with token refresh and revocation etc.
The API requests will be sent by anonymous users. No Login/register functionality is present.
I need to authenticate the API requests, one primitive way I tried was to send an auth key in each request. This auth key, I is saved in the Angular frontend as a constant.
There must be a better and more sophisticated way, kindly help!
Django REST framework largely assumes that requests are authenticated based on a user, but they do provide support for authentication anonymous requests. While this largely breaks from the assumption that "authentication" means "verifying a (Django) user is genuine", Django REST framework does allow it to happen and just substitutes the AnonymousUser instead.
Authentication in DRF can define both the request.user (the authenticated user) and request.auth (generally the token used, if applicable) properties on the request. So for your authentication, you would be holding on to tokens you have created (in a model or somewhere else) and those would be validated instead of the user credentials, and you would just end up not setting the user.
from django.contrib.auth.models import AnonymousUser
from rest_framework import authentication
from rest_framework import exceptions
class ExampleAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
auth = authentication.get_authorization_header(request)
if not auth or auth[0].lower() != b'token':
return None
if len(auth) == 1:
msg = _('Invalid token header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid token header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
try:
token = Token.objects.get(token=auth[1])
except Token.DoesNotExist:
raise exceptions.AuthenticationFailed('No such token')
return (AnonymousUser(), token)
This example assumes that you have a Token model which stores the tokens that will be authenticated. The token objects will be set to request.auth if the request was authenticated properly.
Read the rest api docs on authentication and their tutorial - they offer a solid intro to the options.
I want to login a user using the python-social-auth functionality for Google Plus signin in Django. When logging in from my website, everything works fine and the correct details are added to the database.
However, I want to authenticate from my Android application as well. The user logs in in the application, which then sends the access token to the django API, which handles the login process in the following code, adapted from the documentation:
#csrf_exempt
#serengeti_api_request
#psa('social:complete')
def login_social_token(request, backend):
# Ensure the token has been specified.
token = request.META.get('HTTP_ACCESSTOKEN')
if token is None:
raise SerengetiApiRequestException('Access token is missing!')
# Login the user for this session
user = request.backend.do_auth(token)
if user is None:
raise SerengetiApiRequestException('Could not authenticate user!')
login(request, user)
# Store the email address if one has been specified (e.g. Twitter)
email = request.META.get('HTTP_EMAIL')
if email is not None:
user.email = email
user.save()
# Prepare the parameters to be returned
response = dict({
'id': user.id,
'first_name': user.first_name,
'last_name': user.last_name,
'api_key': request.session.session_key,
})
# Return a 200 status code to signal success.
return HttpResponse(json.dumps(response, indent=4), status=200)
When logging in from the website, the social_auth_usersocialauth table contains:
id | provider | uid | extra_data
==========================================
10 | google-oauth2 | <myemail> | {"token_type": "Bearer", "access_token": "<token>", "expires": 3600}
However, when logging in from the application using the above function, the operation completes ok, but the entry in the table looks like this:
id | provider | uid | extra_data
=========================================
10 | google-oauth2 | <empty> | {"access_token": "", "expires": null}
Also, the auth_user table contains a username like eeed494412obfuscated48bc47dd9b instead of the Google Plus username and the email field is empty.
What am I doing wrong and how can I obtain the same functionality as I get on the website?
I would like to mention that I have implemented Facebook and Twitter authentication from the Android application, which call the above-mentioned function and store the correct details, only Google Plus is causing problems.
Just wanted to share an alternative way of doing this. This example is quite primitive and doesn't cover all cases (e.g. failed authentication). However, it should give enough insight into how OAuth2 authentication can be done.
Obtain CLIENT ID
Obtain a CLIENT ID from OAuth2 service provider (e.g. Google) and configure redirect URLs.
I assume you have already done this.
Create a login / registration link
You need to generate a login / registration link in your view. It should be something like this:
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={{CLIENT_ID}}&redirect_uri={{REDIRECT_URL}}&scope=email
Replace {{CLIENT_ID}} and {{REDIRECT_URL}} with the details you obtained in the previous step.
Create a new view
In urls.py add something like:
url(r'^oauth2/google/$', views.oauth2_google),
In your views.py create a method:
def oauth2_google(request):
# Get the code after a successful signing
# Note: this does not cover the case when authentication fails
CODE = request.GET['code']
CLIENT_ID = 'xxxxx.apps.googleusercontent.com' # Edit this
CLIENT_SECRET = 'xxxxx' # Edit this
REDIRECT_URL = 'http://localhost:8000/oauth2/google' # Edit this
if CODE is not None:
payload = {
'grant_type': 'authorization_code',
'code': CODE,
'redirect_uri': REDIRECT_URL,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
}
token_details_request = requests.post('https://accounts.google.com/o/oauth2/token', data=payload)
token_details = token_details_request.json()
id_token = token_details['id_token']
access_token = token_details['access_token']
# Retrieve the unique identifier for the social media account
decoded = jwt.decode(id_token, verify=False)
oauth_identifier = decoded['sub']
# Retrieve other account details
account_details_request = requests.get('https://www.googleapis.com/plus/v1/people/me?access_token=' + access_token)
account_details = account_details_request.json()
avatar = account_details['image']['url']
# Check if the user already has an account with us
try:
profile = Profile.objects.get(oauth_identifier=oauth_identifier)
profile.avatar = avatar
profile.save()
user = profile.user
except Profile.DoesNotExist:
user = User.objects.create_user()
user.save()
profile = Profile(user=user, oauth_identifier=oauth_identifier, avatar=avatar)
profile.save()
user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, user)
return redirect('/')
You might need the following imports:
from django.shortcuts import redirect
import jwt # PyJWT==0.4.1
import requests # requests==2.5.0
import json
I have a project (not running actually) with google oauth2 authentication. I leave here my config file so it may be useful to you (I was only using oauth2 so some things may vary):
AUTHENTICATION_BACKENDS = (
'social.backends.google.GoogleOAuth2', # /google-oauth2
'django.contrib.auth.backends.ModelBackend',
)
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'your google oauth 2 key'
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'your secret google oauth 2 key'
SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_details',
'social.pipeline.social_auth.social_uid',
'social.pipeline.social_auth.auth_allowed',
'social.pipeline.social_auth.associate_by_email',
'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username',
'social.pipeline.user.create_user',
'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details'
)
I attach the view also (note that I'm using django rest framework).
class ObtainAuthToken(APIView):
permission_classes = (permissions.AllowAny,)
serializer_class = AuthTokenSerializer
model = Token
# Accept backend as a parameter and 'auth' for a login / pass
def post(self, request, backend):
if backend == 'auth': # For admin purposes
serializer = self.serializer_class(data=request.DATA)
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user'])
return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
# Here we call PSA to authenticate like we would if we used PSA on server side.
user = register_by_access_token(request, backend)
# If user is active we get or create the REST token and send it back with user data
if user and user.is_active:
token, created = Token.objects.get_or_create(user=user)
return Response({'id': user.id, 'name': user.username, 'token': token.key})
else:
return Response("Bad Credentials, check the Access Token and/or the UID", status=403)
#strategy('social:complete')
def register_by_access_token(request, backend):
# This view expects an access_token GET parameter
token = request.GET.get('access_token')
backend = request.strategy.backend
user = backend.do_auth(access_token=token, backend=backend)
if user:
# login(request, user) #Only useful for web..
return user
else:
return None
and in the urls.py:
urlpatterns = patterns(
'',
url(r'^login/(?P<backend>[\w-]+)$', ObtainAuthToken.as_view(), ),
)
Sorry for attaching all this code and not providing a specific answer but more data is needed because the error can come from many sources (bad api keys, bad settings configuration, pipeline..). I hope the code helps.
I finally figured it out myself. According to this article in the Android's Google Plus documentation, I also need to request the plus.profile.emails.read scope when making the request in the Android app. Once I added this, the python-social-auth code managed to store the email properly in the uid fields. This allows it to recognize the same user whether logging in from the website or the app, which is what I needed. Here's the scopes string I use:
String scopes = "oauth2:" + Plus.SCOPE_PLUS_LOGIN + " https://www.googleapis.com/auth/plus.profile.emails.read";
However, the extra_data field still contains the values I mentioned above. I believe this is due to needing to request offline access as well, which would allow Google Plus to pass the missing fields back to python-django-auth. More details can be found here.
I've been running into the same problem. The reason why the extra_fields on your google user isn't being set is because python-social-auth calls the google server to set those things, but if you're calling Google with just an access_token, it won't be enough to get Google to return the refresh_token and all those other auth related fields. You can hack it by setting them manually, but then you'd end up using the same access and refresh tokens as the client. Google recommends that you use the client to generate a new authorization token with whatever scopes you need, and then send that auth token to the server, which then will turn it into an access and refresh token. See here for the details (it's a bit of an involved read): https://developers.google.com/identity/protocols/CrossClientAuth
If you're really committed to doing this in the scope of what python-social-auth does, I'd recommend making a custom auth backend, call it GoogleOAuth2AuthorizationCodeAuth (see here for details).
The lazier and probably easy-to-break and gross way is to post the access_token to my server to sign in as a google user (which you're doing properly, it seems), and then later, get another authorization token from the client in order to post to a separate endpoint, which I'll then handle turning into another Credentials model object that's connected to a user profile.
In DjangoRestFramework:
class GoogleAuthorizationCodeView(APIView):
def post(self, request, format=None):
credentials = flow.step2_exchange(code)
saved_creds = GoogleCredentials.objects.create(credentials=credentials)
return Response(status=status.HTTP_201_CREATED)