SAML 2.0 Service Provider in Python - python

I am looking to implement a SAML 2.0 based service provider in Python.
My web apps are currently all Flask applications. I plan to make a Flask blueprint/decorator that allows me to drop single sign-on capabilities into preexisting applications.
I have looked into python-saml extensively and unfortunately there are dependency issues that are not worth resolving, as I have too many preexisting servers/apps whos environments won't be compatible.
PySAML2 looks like it could work, however there is little documentation, and what documentation is available I have trouble comprehending. There are no examples of PySAML2 used in a Flask app.
The Identity Provider I have is Okta. I have Okta set up so that after I login at Okta, I am redirected to my app.
Can anyone offer any advice on using PySAML2, or perhaps advice on how to best authenticate a user using SAML 2.0 who is visiting my application?

Update: A detailed explanation on using PySAML2 with Okta is now on developer.okta.com.
Below is some sample code for implementing a SAML SP in Python/Flask. This sample code demonstrates several things:
Supporting multiple IdPs.
Using Flask-Login for user management.
Using the "SSO URL" as the audience restriction (to simplify configuration on the IdP).
Just in time provisioning of users ("SAML JIT")
Passing additional user information in Attribute Statements.
What is not demonstrated is doing SP initiated authentication requests - I'll followup with that later.
At some point, I hope to create a wrapper around pysaml2 that has opinionated defaults.
Lastly, like python-saml, the pysaml2 library makes use of the xmlsec1 binary. This might also cause dependency issues in your server environments. If that's the case, you'll want to look into replacing xmlsec1 with the signxml library.
Everything in the sample below should work with the following setup:
$ virtualenv venv
$ source venv/bin/activate
$ pip install flask flask-login pysaml2
Finally, you'll need to do to things on the Okta side for this to work.
First: In the General tab of your Okta application configuration, configure the application to send the "FirstName" and "LastName" Attribute Statements.
Second: In the Single Sign On tab of your Okta application configuration, take of the url and put them in a file named example.okta.com.metadata. You can do this with a command like the one below.
$ curl [the metadata url for your Okta application] > example.okta.com.metadata
Here is what you'll need for your Python/Flask application to handle IdP initiated SAML requests:
# -*- coding: utf-8 -*-
import base64
import logging
import os
import urllib
import uuid
import zlib
from flask import Flask
from flask import redirect
from flask import request
from flask import url_for
from flask.ext.login import LoginManager
from flask.ext.login import UserMixin
from flask.ext.login import current_user
from flask.ext.login import login_required
from flask.ext.login import login_user
from saml2 import BINDING_HTTP_POST
from saml2 import BINDING_HTTP_REDIRECT
from saml2 import entity
from saml2.client import Saml2Client
from saml2.config import Config as Saml2Config
# PER APPLICATION configuration settings.
# Each SAML service that you support will have different values here.
idp_settings = {
u'example.okta.com': {
u"metadata": {
"local": [u'./example.okta.com.metadata']
}
},
}
app = Flask(__name__)
app.secret_key = str(uuid.uuid4()) # Replace with your secret key
login_manager = LoginManager()
login_manager.setup_app(app)
logging.basicConfig(level=logging.DEBUG)
# Replace this with your own user store
user_store = {}
class User(UserMixin):
def __init__(self, user_id):
user = {}
self.id = None
self.first_name = None
self.last_name = None
try:
user = user_store[user_id]
self.id = unicode(user_id)
self.first_name = user['first_name']
self.last_name = user['last_name']
except:
pass
#login_manager.user_loader
def load_user(user_id):
return User(user_id)
#app.route("/")
def main_page():
return "Hello"
#app.route("/saml/sso/<idp_name>", methods=['POST'])
def idp_initiated(idp_name):
settings = idp_settings[idp_name]
settings['service'] = {
'sp': {
'endpoints': {
'assertion_consumer_service': [
(request.url, BINDING_HTTP_REDIRECT),
(request.url, BINDING_HTTP_POST)
],
},
# Don't verify that the incoming requests originate from us via
# the built-in cache for authn request ids in pysaml2
'allow_unsolicited': True,
'authn_requests_signed': False,
'logout_requests_signed': True,
'want_assertions_signed': True,
'want_response_signed': False,
},
}
spConfig = Saml2Config()
spConfig.load(settings)
spConfig.allow_unknown_attributes = True
cli = Saml2Client(config=spConfig)
try:
authn_response = cli.parse_authn_request_response(
request.form['SAMLResponse'],
entity.BINDING_HTTP_POST)
authn_response.get_identity()
user_info = authn_response.get_subject()
username = user_info.text
valid = True
except Exception as e:
logging.error(e)
valid = False
return str(e), 401
# "JIT provisioning"
if username not in user_store:
user_store[username] = {
'first_name': authn_response.ava['FirstName'][0],
'last_name': authn_response.ava['LastName'][0],
}
user = User(username)
login_user(user)
# TODO: If it exists, redirect to request.form['RelayState']
return redirect(url_for('user'))
#app.route("/user")
#login_required
def user():
msg = u"Hello {user.first_name} {user.last_name}".format(user=current_user)
return msg
if __name__ == "__main__":
port = int(os.environ.get('PORT', 5000))
if port == 5000:
app.debug = True
app.run(host='0.0.0.0', port=port)

Related

Passing object between flask requests, specifically intuit-oauth AuthClient

I am trying to write a flask application to integrate with quickbooks online api, and am having trouble with authentication. Following their guide for python I have put together the code below. Currently my problem is that I can't figure out how to pass the AuthClient object between requests. I assumed I could just use flask sessions but the AuthClient object when recalled from sessions is incomplete, it only contains the shell and none of the populated data as far as I can tell.
Do I need to try to subclass the AuthClient and rewrite the methods used for pickling? If so any hints on how to get started there would be very helpful.
Also, if anyone has any experience integrating with quickbooks tips on that would be helpful.
from flask import Flask, session, redirect, request
from flask_session import Session
from intuitlib.client import AuthClient
from intuitlib.enums import Scopes
import os
import requests
SECRET_KEY = 'something'
DEBUG = True
REDIS_URL = 'redis://10.74.10.235:6379/0'
app = Flask(__name__)
app.debug = DEBUG
app.secret_key = SECRET_KEY
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SECRET_KEY'] = SECRET_KEY
app.config['REDIS_URL'] = REDIS_URL
sess = Session()
sess.init_app(app)
#app.route('/auth')
def auth():
client_id = os.environ['CLIENT_ID']
client_secret = os.environ['CLIENT_SECRET']
redirect_uri = os.environ['REDIRECT_URI']
environment = os.environ['ENVIRONMENT']
auth_client = AuthClient(client_id, client_secret, redirect_uri, environment)
url = auth_client.get_authorization_url([Scopes.ACCOUNTING])
session['auth_client'] = auth_client
return redirect(url)
#app.route('/callback')
def callback():
auth_client = session['auth_client']
state = str(request.args.get('state'))
auth_code = str(request.args.get('auth_code'))
realm_id = str(request.args.get('realm_id'))
auth_client.get_bearer_token(auth_code, realm_id=realm_id)
return 'boobs'
if __name__ == '__main__':
app.run()
Welp, I was trying to save the wrong info. Found an example app that actually used the intuitlib module here. https://github.com/IntuitDeveloper/SampleOAuth2_UsingPythonClient/blob/master/app/views.py
Turns out instead of passing the AuthCLient around you recreate it each time giving it extra arguments as you acquire them such as:
auth_client = AuthClient(
settings.client_id,
settings.client_secret,
settings.redirect_uri,
settings.environment,
access_token=session.get('access_token', None),
refresh_token=session.get('refresh_token', None),
id_token=session.get('id_token', None),
)
Saving just the tokens in the session.
Still having some troubles with getting the bearer token and a 400 error but this issue is resolved.

Using Flask BasicHTTPAuth with Google Cloud Functions

I'm having difficulty with my Cloud Function in GCP that is simply supposed to return the raw XML stored in a GCS Bucket when invoked with a basic GET request. It works fine without any type of authentication, however since I added the Flask-HTTPAuth package to the mix in order to add some measure of security before exposing the endpoint, the application deploys fine, but crashes without any sort of hint as to why as soon as it is invoked. The error in SD Logging is as follows:
severity: "DEBUG"
textPayload: "Function execution took 1847 ms, finished with status: 'crash'"
timestamp: "2020-07-15T17:22:15.158036700Z"
The function in question (anonymized):
from flask import Flask, request, jsonify, make_response, abort
from flask_httpauth import HTTPBasicAuth
from google.cloud import storage, secretmanager
import google.cloud.logging
import logging
import sys
app = Flask(__name__)
auth = HTTPBasicAuth()
PROJECT_ID = 'example_project'
GCS_BUCKET = 'example_bucket'
users = ['example_user']
# Instantiate logger
client = google.cloud.logging.Client()
client.get_default_handler()
client.setup_logging()
#auth.verify_password
def verify_password(username, password):
# Instantiate the Secret Manager client.
sm_client = secretmanager.SecretManagerServiceClient()
# Load secrets
name = sm_client.secret_version_path(PROJECT_ID, 'example_secrets_ref', 1)
secrets_pass = sm_client.access_secret_version(name)
passwords = [secrets_pass]
if username in users and password in passwords:
logging.info('auth success')
return username
logging.info('auth fail')
return abort(403)
#app.route('/')
#auth.login_required
def latest_xml():
try:
request_json = request.get_json()#silent=True)
storage_client = storage.Client(project=PROJECT_ID)
bucket = storage_client.get_bucket(GCS_BUCKET)
blob = bucket.get_blob('latest_pull.xml')
latest_xml = blob.download_as_string()
logging.info('Loaded blob from GCS')
return(latest_xml)
except exception as e:
logging.error(str(e))
logging.error("Failed to load blob from GCS")
sys.exit(1)
if __name__ == '__main__':
app.run()
I've tried setting the entrypoint as both the main function as well as the auth function to no avail. My question is: is it possible to even use basic auth in a GCP Cloud Function or am I barking up the wrong tree here?
Your function doesn't enforce the standard signature for http function
def latest_xml(request):
...
Here you use a flask web server, which is not need, and not used by Cloud Functions. However, I recommend you to have a look to Cloud Run, and to add a simple and generic Dockerfile to deploy . You can deploy your "function" as-is in a container and to have the same behavior as Cloud Functions.
EDIT
When you use flask, the request object is global for each request. You use it like this:
request_json = request.get_json()#silent=True)
With Cloud Functions, this object is caught by the Cloud Functions platform and passed in parameter to your function.
In the request object, you have the body of the request, useless in GET for example. But also, all the request context: headers, user agent, source ip,...

Post API-- what are the steps that need to be followed once the code is created so that I can add data into a txt file through this API

I am new to API, and get a tasks of creating POST API. I have created a code somehow.
I want to add data to the hello.txt through post API, So how will I do it?
Here is my code:
import flask
from flask import request, jsonify
app = flask.Flask(__name__)
app.config["DEBUG"] = True
#app.route('/api/v1/resources/messages', methods = ['POST'])
def api_message():
if request.headers['Content-Type'] == 'text/plain':
return "Text Message: " + request.data
elif request.headers['Content-Type'] == 'application/octet-stream':
return "Binary message written!"
elif request.headers['Content-Type'] == 'application/json':
f = open('F:\Asif_Ahmed\Projects\api\hello.txt',"w")
f.write(request.data)
f.close()
return "JSON Message: " + json.dumps(request.json)
else:
return "415 Unsupported Media Type ;)"
app.run()
from flask import Flask, jsonify, render_template, request #import flask library
from flask_basicauth import BasicAuth # import flask library for create basic authentication if needed
from flask_cors import CORS # import flask library Cross-Origin Resource Sharing that is a mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected resources from a server at a different origin
app = Flask(__name__)
CORS(app) #set-up cors for my app
#if you want use basic authentication you need set-up username and password
app.config['BASIC_AUTH_USERNAME'] = 'admin'
app.config['BASIC_AUTH_PASSWORD'] = 'password'
basic_auth = BasicAuth(app)#set-up username and password for my app but in this case I'm not specifying yet in which API use them
#app.route('/api/v1/resources/add_messages', methods=['POST'])#create my POST api
#basic_auth.required# set-up basic authentication for this API, comment out if not needed
def update_credential ():
json_credential=request.get_json()#get the JSON sent via API
print (json_credential["message"])#get the node "message" of my JSON
###########
#code to write in your file, you need write the json_credential["message"]
###########
return ("ok")
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1024, threaded=True)#start my flask app with local_host IP and specific port, if you don't specify the port it will run in the default port
In this case the JSON Input should be:
{"message":"your text"}
Please let me know if something is not clear, I even try this code on my local and the JSON is passed without problems.....
So you need run your python script and see that the API is running, if you had no JSON to send and was just a simple API that give back information you should have used even Chrome but in this case that you need send some JSON data I would advice you to use Postman.
See screenshot example:

Proper use of Flask-ldap

I am currently trying to use Flask-LDAP for authentication purposes for Sandman. Unlike normal python LDAP where the documentation is pretty straight forward, this is pretty offbase.
Example from documentation.
from flask import Flask
from flask.ext.ldap import LDAP, login_required
from flask.ext.pymongo import PyMongo
app = Flask(__name__)
app.debug = True
app.config['LDAP_HOST'] = 'ldap.example.com'
app.config['LDAP_DOMAIN'] = 'example.com'
app.config['LDAP_SEARCH_BASE'] = 'OU=Domain Users,DC=example,DC=com'
app.config['LDAP_LOGIN_VIEW'] = 'custom_login'
app.config['MONGO_DBNAME'] = 'simpledb'
mongo = PyMongo(app, config_prefix='MONGO')
ldap = LDAP(app, mongo)
app.secret_key = "welfhwdlhwdlfhwelfhwlehfwlehfelwehflwefwlehflwefhlwefhlewjfhwelfjhweflhweflhwel"
app.add_url_rule('/login', 'login', ldap.login, methods=['GET', 'POST'])
This does not tell me where I can place my dn or password. So I am completely confused as to how this thing actually works.
Has anyone effectively used Flask-LDAP and if so how did you set it up? Or is there a better product out there like flask-login that I should use?
Thanks,
Flask-login is okay and the de-facto lib for session management so you'll be needing that.
As for flask-ldap, yeah it's not well documented...
But this might be useful to you (it seems like a full feature ldap lib for flask including login helpers):
flask_ldap_login at https://github.com/ContinuumIO/flask-ldap-login

Python Flask Decorators and Apache mod_wsgi

I have created a fairly strait forward example of the Flask/Python application, and then I have decided to split code to the two files, so I have moved all decorators for Authentication to separate file, like following:
#### test.py
from flask import Flask, request, abort, request, make_response, url_for, jsonify
from flask.ext.httpauth import HTTPBasicAuth
import pypyodbc, json, collections, _db, _sc
from customauth import CustomHTTPBasicAuth
app = Flask(__name__)
app.debug = True
from werkzeug.debug import DebuggedApplication
application = DebuggedApplication(app, evalex=True)
#app.errorhandler(400)
def not_found(error): return make_response(jsonify( { 'error': 'Bad request' } ), 400)
#app.errorhandler(404)
def not_found(error): return make_response(jsonify( { 'error': 'Not found' } ), 404)
auth = CustomHTTPBasicAuth()
#app.route('/hello')
#auth.login_required
def hello_world():
name = request.args.get('name','')
return 'Hello ' + name + '!'
if __name__ == '__main__':
app.run()
#### customauth.py
from flask import Flask, jsonify, make_response, request
from flask.ext.httpauth import HTTPBasicAuth
import md5, socket
class CustomHTTPBasicAuth(HTTPBasicAuth):
def __init__(self):
super(CustomHTTPBasicAuth, self).__init__()
#self.get_password
def get_password(username):
if (md5.new(username).hexdigest() == '0cc175b9c0f1b6a831c399e269772661'): #'0cc175b9c0f1b6a831c399e269772661' is md5('a')
return '0cc175b9c0f1b6a831c399e269772661'
return None
#self.hash_password
def custom_authenticate(password):
return '0cc175b9c0f1b6a831c399e269772661'
#self.error_handler
def unauthorized():
return make_response(jsonify( { 'error': 'Unauthorized access' } ), 401)
#### wsgi.py
import sys
sys.path.insert(0, "c:/testflask/test")
from flask1 import app
application = app
First issues is: this code works fine when executed with the python test.py
Authentication form will popup asking for username and password, after entering 'a','a' authentication will pass and result 'Hello + whatever' will display.
But when executed through Apache mod_wsgi it does not run as expected authentication form will display unlimited number of times.
Please can someone explain this code running under python and not under mod_wsgi, and is there a way to fix this?
I am thinking that I am doing something wrong with the decorators but I am not sure what, also it does not answer why it's running via python and not via mod_wsgi.
Another thing is how to get Debug page via apache mod_wsgi instead of "Internal Server Error" page? I have tried with 'werkzeug.debug' and some other suggested things from the internet but nothing really worked for me.
Thanks,
#
I have found solution for the first bullet it is Apache settings only thing needed is to add WSGIPassAuthorization On in the config file.
Still trying to find solution for debugging...
The Flask debugger will likely not work if it detects that it is running in a multi process configuration as would often be the case for mod_wsgi if you are using embedded mode. Ensure you are using daemon mode of mod_wsgi:
WSGIDaemonProcess mysite
WSGIProcessGroup mysite
Note, do NOT set 'processes=1' to WSGIDaemonProcess. Let it default to 1. Using 'processes' option with any value will cause wsgi.multiprocess to be True which is possibly what is causing the Flask debugger to be disabled.

Categories

Resources