I'm trying to implement SSO in a Web Application using OpenID Connect.
I'm trying to replicate the example in request-oauthlib for Web Application but without success
OAuth2/OpenID Provider
flask
and 2 test endpoint: /login
that redirects to the IdP and retrieves (authorization_url
and state
) and /callback
that using the client_id
, client_secret
, state
and authorization_response
tries to retrieve the access tokenUnfortunately when I try to retrieve the access token I receive back the following error: oauthlib.oauth2.rfc6749.errors.InvalidClientError: (invalid_client) Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)
import json
import os.path
from uuid import uuid4
from requests_oauthlib import OAuth2Session
from waitress import serve
from flask import Flask, jsonify, request, url_for, redirect, session
from pprint import pprint
with open(os.path.join("Config", "client_secrets.json"), "r") as f:
idp = json.load(f)
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
def main():
app = Flask(__name__)
# This allows us to use a plain HTTP callback
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = "1"
app.config['SECRET_KEY'] = str(uuid4())
@app.route('/')
def index():
return """
<a href="/login">Login</a>
"""
@app.route("/login")
def login():
oauth = OAuth2Session(client_id=idp["client_id"],
scope=idp["scope"],
redirect_uri=idp["callback"]
)
authorization_url, state = oauth.authorization_url(idp["authorize"])
session['oauth_state'] = state
return redirect(authorization_url)
@app.route("/callback")
def callback():
pprint(request.__dict__)
oauth = OAuth2Session(client_id=idp["client_id"],
state=session['oauth_state']
)
# When I try to get the token, nothing works
token = oauth.fetch_token(
idp["token"],
client_secret=idp["client_secret"],
authorization_response=request.url
)
# I never reach this line
session['oauth_token'] = token
return "I cannot see this :("
print("Starting webserver")
serve(app, host='0.0.0.0', port=5000)
print("Webserver running")
if __name__ == "__main__":
main()
{'cookies': ImmutableMultiDict([('session', 'REDACTED_SESSION')]),
'environ': {'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br',
'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.5',
'HTTP_CONNECTION': 'keep-alive',
'HTTP_COOKIE': 'session=REDACTED_SESSION',
'HTTP_DNT': '1',
'HTTP_HOST': 'localhost:5000',
'HTTP_SEC_FETCH_DEST': 'document',
'HTTP_SEC_FETCH_MODE': 'navigate',
'HTTP_SEC_FETCH_SITE': 'cross-site',
'HTTP_UPGRADE_INSECURE_REQUESTS': '1',
'HTTP_USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; '
'rv:123.0) Gecko/20100101 Firefox/123.0',
'PATH_INFO': '/callback',
'QUERY_STRING': 'code=REDACTED_CODE&state=REDACTED_STATE',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_HOST': '127.0.0.1',
'REMOTE_PORT': '64951',
'REQUEST_METHOD': 'GET',
'REQUEST_URI': '/callback?code=REDACTED_CODE&state=REDACTED_STATE',
'SCRIPT_NAME': '',
'SERVER_NAME': 'waitress.invalid',
'SERVER_PORT': '5000',
'SERVER_PROTOCOL': 'HTTP/1.1',
'SERVER_SOFTWARE': 'waitress',
'waitress.client_disconnected': <bound method HTTPChannel.check_client_disconnected of <waitress.channel.HTTPChannel connected 127.0.0.1:64951 at 0x285f5356ba0>>,
'werkzeug.request': <Request 'http://localhost:5000/callback?code=REDACTED_CODE&state=REDACTED_STATE' [GET]>,
'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>,
'wsgi.file_wrapper': <class 'waitress.buffers.ReadOnlyFileBasedBuffer'>,
'wsgi.input': <_io.BytesIO object at 0x00000285F5391E90>,
'wsgi.input_terminated': True,
'wsgi.multiprocess': False,
'wsgi.multithread': True,
'wsgi.run_once': False,
'wsgi.url_scheme': 'http',
'wsgi.version': (1, 0)},
'headers': EnvironHeaders([('Host', 'localhost:5000'), ('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0'), ('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'), ('Accept-Language', 'en-US,en;q=0.5'), ('Accept-Encoding', 'gzip, deflate, br'), ('Dnt', '1'), ('Connection', 'keep-alive'), ('Cookie', 'session=REDACTED_SESSION'), ('Upgrade-Insecure-Requests', '1'), ('Sec-Fetch-Dest', 'document'), ('Sec-Fetch-Mode', 'navigate'), ('Sec-Fetch-Site', 'cross-site')]),
'host': 'localhost:5000',
'json_module': <flask.json.provider.DefaultJSONProvider object at 0x00000285F5354530>,
'method': 'GET',
'path': '/callback',
'query_string': b'code=REDACTED_CODE&state=REDACTED_'
b'_STATE',
'remote_addr': '127.0.0.1',
'root_path': '',
'scheme': 'http',
'server': ('waitress.invalid', 5000),
'shallow': False,
'url': 'http://localhost:5000/callback?code=REDACTED_CODE&state=REDACTED_STATE',
'url_rule': <Rule '/callback' (GET, OPTIONS, HEAD) -> callback>,
'view_args': {}}
When I tried to instantiate the OAuth2Session object it wasn't creating the correct object:
oauth = OAuth2Session(client_id=idp["client_id"],
state=session['oauth_state']
### MISSING PARAMETER: redirect_uri
)
The solution is to instantiate the OAuth2Session object passing the same parameters both in login and callback:
oauth = OAuth2Session(client_id=idp["client_id"],
scope=idp["scope"],
redirect_uri=idp["callback"]
)
To be super clear this is the right callback function:
@app.route("/callback")
def callback():
pprint(request.__dict__)
oauth = OAuth2Session(client_id=idp["client_id"],
state=session['oauth_state'],
redirect_uri=idp["callback"]
)
# When I try to get the token, nothing works
token = oauth.fetch_token(
idp["token"],
client_secret=idp["client_secret"],
authorization_response=request.url
)
# I never reach this line
session['oauth_token'] = token
return "Ok, everithing is working"