JWT authentication between Django and Vue.js - django

I wish to create a public area on a website containing standard Django and templates. When a user logs in to the members area, they are logged in to a SPA (using Vue.js) and Django Rest Framework. I will be using JWT's to handle authentication between the SPA and backend once the user has logged in to the members area.
This is the user journey:
User browses around the public areas of the site (served by normal Django and templates).
User decides to signup / login to the members area.
Django Rest Framework generates a JWT for the user and returns the token along with the index.html of the SPA
The user continues to use the SPA with the JWT
Is the above possible and how would it be done? More specifically, the issue is that the user is not logging in to the SPA and requesting a JWT. They are logging in to regular Django and getting returned a JWT along with the SPA. That JWT would then be used from that point onwards.

This is something that I've used with laravel, but the principle should be the same.
I've placed vue-cli generated code into the subfolder frontend.
This is trimmed content of the file vue.config.js, which you need to add manually to the vue-cli project root.
const path = require('path')
/*
vue-cli is initialized in project subfolder `frontend`
and I run `npm run build` in that sub folder
*/
module.exports = {
outputDir: path.resolve(__dirname, '../public/'),
/*
https://cli.vuejs.org/config/#outputdir
!!! WARNING !!! target directory content will be removed before building
where js, css and the rest will be placed
*/
assetsDir: 'assets/',
/*
Where `public/index.html` should be written
- this is example for the laravel, but you can change as needed
- .blade.php is laravel template that's served trough laravel.
So you could inject JWT into `index.html`
- Check https://cli.vuejs.org/guide/html-and-static-assets.html
for the syntax before adding values
*/
indexPath: path.resolve(__dirname, '../resources/views/index.blade.php'),
devServer: {
host: '0.0.0.0',
port: 8021,
proxy: {
/*
Proxy calls from
localhost:8021/api (frontend)
localhost:8020/api (backend)
*/
'/api': {
target: 'http://localhost:8020',
changeOrigin: true,
}
}
}
};

Related

flask sessions not persistent between requests from cross domains

I have a frontend vue site hosted on google's firebase with the url (https://front-end.web.com) , while my flask backend is hosted on heroku with the url (https://back-end.heroku.com). This makes my session not to persist across requests, I tried fixing this by implementing CORS on my backend, but for some reason it's not working , below are snippets of my code to show my implementation
config_class.py
class ConfigClass():
CORS_ALLOW_HEADERS = ['Content-Type']
CORS_ORIGINS = ['https://front-end.web.com']
SECRET_KEY = os.environ.get("APP_SECRET_KEY")
SESSION_TYPE = 'redis'
_init.py
from flask import Flask, session
from flask_session import Session
from flask_cors import CORS
from root_folder.config import ConfigClass
db = SQLAlchemy()
migrate = Migrate()
ma = Marshmallow()
sess = Session()
def create_app(ConfigClass):
# initiate the flask app and assign the configurations #
app = Flask(__name__)
app.config.from_object(config_options[config_class])
sess.init_app(app)
from root_folder.clients import clients_app
# register all the blueprints in this application
app.register_blueprint(clients_app)
CORS(app, supports_credentials=True)
# return the app object to be executed
return app
app.py
from root_folder import create_app
app = create_app()
Procfile:
web: gunicorn -w 1 app:app
axios front end request
let formData = new FormData();
formData.append("email", email);
formData.append("password", password);
axios.post(
backendUrl+'create_client_account',
formData,
{
withCredentials: true,
headers:{
"Content-Type": "multipart/form-data"
}
}
);
create client route ( I have stripped this code block to the bare minimum to make it understandable):
from flask import session
# route for creating account credentials
#bp_auth_clients_app.route("/create_client", methods=["POST"])
def create_client():
username = request.form.get("username").lower()
email = request.form.get("email").lower()
# create account code goes here #
auth_authentication = True
session["auth_authentication"] = auth_authentication
req_feedback = {
"status": True,
"message": "Account was successfully created",
"data": feedback_data
}
return jsonify(req_feedback), 200
After the account is successfully created, I am unable to access the session value in subsequent requests, it returns None.
To recreate the problem on my local server, I access the front-end via the domain "localhost:8080" , while I access the flask server via "127.0.0.1:8000" . If I change the front end domain to "127.0.0.1:8080", I don't usually have any problems.
Kindly advice on what to do.
Thanks to Ahmad's suggestion, I was able to resolve the issue using custom domains for both my frontend and backend as follows:
frontend.herokuapp.com -> customDomain.com
backend.herokuapp.com -> api.customDOmain.com
finally I added the line below to my session config:
SESSION_COOKIE_DOMAIN = ".customDomain.com"
And all was well and good.
Sessions use cookies:
On session creation the server will send the cookie value in the set-cookie header. It doesn't work for you because of cross origin issue.
It works fine for you when you use 127.0.0.1 because 127.0.0.1:8080 and 127.0.0.1:8000 are the same origin so the browser accepts the set-cookie header and do set the cookie no problem.
Cookies are sent in the header on each request and your server loads the session from Redis by cookie value (The cookie value is called session_id).
How it gets inserted => Normally your session gets serialized and inserted in Redis with the cookie hash as Key in the end of the request life cycle.
If you want to keep using sessions and cookies you need to find another solution for your deployment to so that your backend and frontend have the same hostname.
If you can't do I'd recommend to read about JWT (Json-Web-Tokens).
EDIT
You can send the session id in your response body and save it in local storage.
Then you need to configure:
frontend set the session id value it in the Authorization header base64 encoded.
Backend base64 decode Authorization header value from request and check for the session in Redis, if exists load it.
EDIT
How to deploy both backend/frontend on same hostname using apache:
using apache you need to create 2 virtual hosts one for backend and the other for frontend listening on different ports then configure your web server deployment to use the backend VH if the path is prefixed by /api/ and use the frontend Virtual host for anything else.
This way any request you make to your api your backend will handle it otherwise it'll serve your frontend app.
This is just a way on how to do it there is plenty others
Check this question.

Django REST to React - getting social auth tokens without password

I want to pass info to React about the current authenticated user within an app that only uses social authentication on the backend (that is processed by social_django). All of my user and user token info is stored within django REST, and to access the tokens, I normally have to send POST requests to rest_framework.authtoken's obtain_auth_token view. My django root urls.py file looks like:
...
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
...
url(r'^obtain-auth-token/$', obtain_auth_token),
...
]
However, in order to actually get the auth tokens associated with the users in my database, I need to supply the username and password within my POST request. Social authentication automatically creates new users without assigning any passwords, so how do I get those tokens?
Have you got this working? If no, here is what I did. Hope it helps.
My Setup:
Django with Postgres
Django Rest Framework for REST API implementation
Python Social Auth (PSA) for Social Authentication (For now using Google+ libraries)
Reactjs frontend
While using Login for login, it translates to /login/google-plus/. This not only get's the acess_token but also creates a "social user" in your database. I used oauth 2.0 client side libraries in my case and roughly followed this approach to fetch the google user object with all the details on the client side. I replaced form in above link with ajax call which is more flexible and gives control to me to access tokens and other information necessary. The ajax call here ensures creation of social user in social auth table within the database.
<script type="text/javascript">
gapi.load('auth2', function () {
let auth2;
auth2 = gapi.auth2.init({
client_id: "YOUR CLIENT ID",
scope: "profile",
cookie_policy: 'single_host_origin'
});
auth2.then(function () {
let button = document.getElementById("google-plus-button");
auth2.attachClickHandler(button, {}, function (googleUser) {
// Send access-token to backend to finish the authenticate
// with your application
let authResponse = googleUser.getAuthResponse();
$.ajax({
"type": "POST",
"url": "/complete/google-plus/",
"data": {
"access_token": authResponse.access_token,
"CSRF": "{% csrf_token %}"
}
}).then(function(data){
console.log(data);
// Your success code
}).fail(function(error){
console.log(error);
});
});
});
});
</script>
Once you fetch the access_tokens you can store them in browser local storage till the user logs out. On log out you can delete them.
This method works well for me for the setup I mentioned. Also the problem of querying /obtain-auth-token with username and password is not there at all.
Would definitely be interested to know if there are other ways of accessing social auth tokens from PSA django. Cheers!

Dropbox and Django SSO using SAML

Summary
I am looking to use Dropbox SSO functionality by using the authentication from a Django site. Note that I'm not looking to use SAML as a backend for my Django site.
Resources
1) Dropbox Custom SSO help page: https://www.dropbox.com/en/help/1921#custom
2) Creating a SAML response: https://robinelvin.wordpress.com/2009/09/04/saml-with-django/
3) Struggled to find any examples from Google of people doing this kind of SSO. Lots of links about people using SAML as a Django backend.
Question
In the dropbox admin settings I can add my X509 certificate and the login link. This means that when you try to login into Dropbox using SSO it nicely forwards you to my Django site's login page using a GET request with a SAMLRequest in the querystring.
However, my understanding is that I now need to, once the user is authenticated on the Django site, fire a POST request back to Dropbox at their SAML login link with a SAMLResponse in the post data. Using the second resource above I believe I can create the SAMLResponse xml but I am unsure how to redirect the user to the dropbox SAML login link with the SAML data from my Django view.
Any help much appreciated.
Managed to get the functionality I needed using django-saml2-idp https://github.com/peopledoc/django-saml2-idp
Good documentation on installing here: https://github.com/peopledoc/django-saml2-idp/blob/master/doc/INSTALL.txt
Settings in the Dropbox Admin console required the X509 certificate and then the login url set to: https://****.com/idp/login
Note that I had issues installing the M2Crypto dependency so used an Ubuntu package via:
sudo apt-get install python-m2crypto
Additionally I'm using Django 1.9.6 so needed to make overrides to the views.py, urls.py, and registry.py files to make them compatible (various import statements needed updating and the urls changed to the new list format rather than using patterns).
Created a Dropbox Processor as follows:
import base64
import zlib
from saml2idp import base
from saml2idp.xml_render import _get_assertion_xml
def get_assertion_dropbox_xml(parameters, signed=False):
return _get_assertion_xml(ASSERTION_DROPBOX, parameters, signed)
ASSERTION_DROPBOX = (
'<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" '
'ID="${ASSERTION_ID}" '
'IssueInstant="${ISSUE_INSTANT}" '
'Version="2.0">'
'<saml:Issuer>${ISSUER}</saml:Issuer>'
'${ASSERTION_SIGNATURE}'
'${SUBJECT_STATEMENT}'
'<saml:Conditions NotBefore="${NOT_BEFORE}" NotOnOrAfter="${NOT_ON_OR_AFTER}">'
'<saml:AudienceRestriction>'
'<saml:Audience>${AUDIENCE}</saml:Audience>'
'</saml:AudienceRestriction>'
'</saml:Conditions>'
'<saml:AuthnStatement AuthnInstant="${AUTH_INSTANT}"'
'>'
'<saml:AuthnContext>'
'<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>'
'</saml:AuthnContext>'
'</saml:AuthnStatement>'
'${ATTRIBUTE_STATEMENT}'
'</saml:Assertion>'
)
class Processor(base.Processor):
def _decode_request(self):
"""
Decodes _request_xml from _saml_request.
"""
self._request_xml = zlib.decompress(base64.b64decode(self._saml_request), -15)
def _format_assertion(self):
self._assertion_xml = get_assertion_dropbox_xml(self._assertion_params, signed=False)
Which you register in your settings.py file as follows:
SAML2IDP_CONFIG = {
'autosubmit': True,
'certificate_file': '/****/certificate.pem',
'private_key_file': '/****/private-key.pem',
'issuer': 'https://www.****.com',
'signing': True,
}
sampleSpConfig = {
'acs_url': 'https://www.dropbox.com/saml_login',
'processor': 'dropbox.Processor',
}
SAML2IDP_REMOTES = {
'sample': sampleSpConfig,
}
Works like a dream. Hope this helps somebody out there.

Getting Facebook callback error even after enabling 'Embedded browser OAuth Login' and specifying the callback url

I have a rails(4.2.0) application that uses Facebook login functionality. The main gems are devise(3.4.0) and omniauth-facebook(2.0.0). I have registered the application on Facebook and have been using its test app for development. The Facebook login functionality works in the development env.
When trying to use the facebook login feature on the production server, I get error as "Given URL is not allowed by the Application configuration: One or more of the given URLs is not allowed by the App's settings. It must match the Website URL or Canvas URL, or the domain must be a subdomain of one of the App's domains."
The details for settings for test app being used in the dev env are as -
Settings:
Basic:
App Domains: 'localhost'
Website:
Site URL: 'http://localhost:3000'
Advanced:
OAuth Settings:
Embedded browser OAuth Login: Yes
Valid OAuth redirect URIs: "http://localhost:3000/users/auth/facebook/callback"
The details for settings for registered app being used in the production env are as -
Settings:
Basic:
App Domains: 'www.mysite.co'
Website:
Site URL: 'http://www.mysite.co'
Advanced:
OAuth Settings:
Embedded browser OAuth Login: Yes
Valid OAuth redirect URIs: "http://www.mysite.co/users/auth/facebook/callback"
I have specified the following in my secrets.yml
development:
secret_key_base: some_secret_key
facebook:
app_id: test_app_id
app_secret: test_app_secret
production:
secret_key_base: some_secret_key
facebook:
app_id: registered_app_id
app_secret: registered_app_secret
And have been using the creds from secrets.yml in the devise initialiser as
# ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
require 'omniauth-facebook'
config.omniauth :facebook, Rails.application.secrets.facebook['app_id'], Rails.application.secrets.facebook['app_secret'], scope: ['user_photos', 'email', 'public_profile']
The actual domain name(blackened) has no typos anywhere and is same wherever it is used.
Contains of routes.rb related to omniauth are as
cat config/routes.rb
Rails.application.routes.draw do
root 'home#index'
devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }
# routes related to other controllers
end
The routes are as below
bundle exec rake routes | grep user
new_user_session GET /users/sign_in(.:format) devise/sessions#new
user_session POST /users/sign_in(.:format) devise/sessions#create
destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
user_omniauth_authorize GET|POST /users/auth/:provider(.:format) users/omniauth_callbacks#passthru {:provider=>/facebook/}
user_omniauth_callback GET|POST /users/auth/:action/callback(.:format) users/omniauth_callbacks#:action
The only code related to omniauth in the entire app is as
$ cat app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
#You need to implement the method below in your model (e.g. app/models/user.rb)
#user = User.from_omniauth(request.env["omniauth.auth"])
if #user.persisted?
sign_in_and_redirect #user, event: :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, kind: "Facebook") if is_navigational_format?
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
Upon further digging the problem, it was observed that the error didnt occur when 'www.example.com' was specified in the url and hence, the callback worked. When 'example.com' was specified in the address bar and facebook login tried, the login crashed with the above error.
So, I fixed the above issue by making some changes to the settings in for the facebook app. I donno if this is the right approach but it worked out. Just making the change as in point 2 didnt solve the problem.
Changes are:
1) Specified the 'App Domains' with 'example.com' and 'www.example.com'
2) Enabled 'Client OAuth Login' to 'Yes'
3) Specified 'Valid OAuth redirect URIs' with 'http://example.com/users/auth/facebook/callback' and 'http://www.example.com/users/auth/facebook/callback'
Ok, so I assume that you have a web app NOT running on Facebook that simply uses the Facebook OAuth flow for login functionality, correct? If so, you must enable "Client OAuth Login" in your application settings for the production environment. If you don't, then the web OAuth flow will not work. See this article: https://developers.facebook.com/docs/facebook-login/security

AngularJS + Django - csrf for separate apps

I have both a Django app and a Angular JS app hosted at different end-points. Obviously in order for XHR requests to work I need to set the csrf token within Angular, which is easy enough to do when Angular is served by Django, but not so much when independent.
Here is my code so far:
angular.module('App', [
'ngCookies',
])
.run(['$rootScope', '$http', '$cookies',
function($rootScope, $http, $cookies){
// Set the CSRF header token to match Django
$http.defaults.headers.post['X-CSRFToken'] = $cookies['csrftoken'];
// Bootstrap
$http.get('http://127.0.0.1:8000/test/').success(function(resp){
console.log($cookies['csrftoken']);
});
}
])
It seems that $cookies['csrftoken'] is always undefined, and I assume I have to retrieve this somehow but can't find any resources as to how this process works.
Can anyone point me in the right direction?
Cookies are only accessible on the same origin, so accessing from another domain won't share the CSRF Token through cookies, you're going to have to find another way to introduce the cookie (such as with Django's template tag).
Second, your example looks likes its trying to read a Cookie from the $http.get() call. The $cookie service collects Cookies from when the document is loaded (stored document.cookie) and the resulting cookies are not accessible from Ajax/XHR calls cross-domain.
You can use this:
app = angular.module("App", []);
app.run(function($http) {
$http.defaults.headers.post['X-CSRFToken'] = $.cookie('csrftoken');
});
where $.cookie comes from jQuery Cookie plugin.