'how to make an api protected in SSO implementation using fastapi
I have written a fastapi web service, which works as service provider and gets authenticated with Okta Single sign on idp using pysaml2 libraries. The login functionality works as expected. On successful saml response from okta idp, I get user attributes. My problem is how to make an api protected after that, so that user can access the given API only when he is authenticated from IDP.
I have some flask implementation for this, eg okta-pysaml2-example on GitHub. However, this is implemented in flask and use flask-login for API protection. But I could not get something similar to that in fastapi.
import json
import fastapi
from fastapi.middleware.cors import CORSMiddleware
from fastapi import APIRouter
from fastapi.responses import JSONResponse, RedirectResponse, PlainTextResponse
from fastapi import Request
from saml2 import (
BINDING_HTTP_POST,
BINDING_HTTP_REDIRECT,
entity,
)
from saml2.client import Saml2Client
from saml2.s_utils import rndstr
from saml2.config import Config as Saml2Config
import requests
import logging
sso_router = sso = APIRouter()
uid2user = {} # global variable to keep user object
class User(object):
def __init__(self, name_id, data, saml_response):
self.name_id = name_id
self.data = data
self.response = saml_response
@property
def authn_statement(self):
xml_doc = xml.dom.minidom.parseString(
str(self.response.assertion.authn_statement[0])
)
return xml_doc.toprettyxml()
def _expiration(timeout, tformat=None):
# Wed, 06-Jun-2012 01:34:34 GMT
if not tformat:
tformat = "%a, %d-%b-%Y %T GMT"
if timeout == "now":
return time_util.instant(tformat)
else:
# validity time should match lifetime of assertions
return time_util.in_a_while(minutes=timeout, format=tformat)
metadata_url_for = {
# For testing with http://saml.oktadev.com use the following:
#'okta': 'https://dev-68931166.okta.com/app/exk384pyifILOYpUY5d7/sso/saml/metadata',
'okta': 'https://saml-bird.daad.com/saml2/idp/metadata.php',
}
def saml_client_for(idp_name=None):
'''
Given the name of an IdP, return a configuation.
The configuration is a hash for use by saml2.config.Config
'''
if idp_name not in metadata_url_for:
raise Exception("Settings for IDP '{}' not found".format(idp_name))
rv = requests.get(metadata_url_for[idp_name])
settings = {
'entityid': 'https://example.com/sp.xml',
'metadata': {
'inline': [rv.text],
},
'service': {
'sp': {
'endpoints': {
'assertion_consumer_service': [
("http://example.com/api/saml/sso/okta", BINDING_HTTP_POST),
("https://example.com/api/saml/sso/okta", BINDING_HTTP_POST)
],
"single_logout_service": [
("http://example.com/api/saml/slo/okta", BINDING_HTTP_REDIRECT),
("http://example.com/api/saml/slo/okta", BINDING_HTTP_POST),
],
},
"required_attributes": ['displayName','mail',],
"metadata_key_usage" : "both",
"enc_cert": "use",
'allow_unsolicited': True,
'authn_requests_signed': False,
'logout_requests_signed': False,
'want_assertions_signed': True,
'want_response_signed': False,
},
},
"key_file": "/app/pki/mykey.pem",
"cert_file": "/app/pki/mycert.pem",
"xmlsec_binary": '/usr/bin/xmlsec1',
'encryption_keypairs': [
{
'key_file': "/app/pki/mykey.pem",
'cert_file': "/app/pki/mycert.pem",
},
],
}
spConfig = Saml2Config()
spConfig.load(settings)
spConfig.allow_unknown_attributes = True
saml_client = Saml2Client(config=spConfig)
return saml_client
@sso.post("/saml/sso/okta")
async def idp_initiated(request: Request):
print("In idp initiated...")
idp_name = 'okta'
saml_client = saml_client_for(idp_name)
form = await request.form()
authn_response = saml_client.parse_authn_request_response(
form.get('SAMLResponse'),
entity.BINDING_HTTP_POST)
authn_response.get_identity()
user = User(authn_response.name_id, authn_response.ava, authn_response)
request.session["user_name_id"] = str(user.name_id)
user_store = {'first_name': authn_response.ava['displayName'][0], 'last_name': authn_response.ava['displayName'][0], 'user_name': authn_response.ava['cn'][0]}
#resp = JSONResponse(user_store)
resp = RedirectResponse("https://example.com/home")
uid = rndstr(32)
uid2user[uid] = user
resp.set_cookie(key='spauthn', value=authn_response.name_id, httponly=True,expires=1800)
resp.status_code = 302
return resp
@sso.get("/saml/login/okta")
def sp_initiated():
print("In sp initiated...")
idp_name = 'okta'
saml_client = saml_client_for(idp_name)
reqid, info = saml_client.prepare_for_authenticate()
redirect_url = None
# Select the IdP URL to send the AuthN request to
for key, value in info['headers']:
if key == 'Location':
redirect_url = value
response = fastapi.responses.RedirectResponse(redirect_url)
response.headers['Cache-Control'] = 'no-cache, no-store'
response.headers['Pragma'] = 'no-cache'
return response
@app.get("/protected_api")
async def protected_api(): # -------------> how to make this api protected so that only logged user can access this.
return "Hi! How are you? You are in a protected Zone."
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
