I'm working on implementing OAuth2 authentication in a FastAPI application using ForgeRock as the identity provider. This setup is within my company's internal environment, and I have the necessary certificates. However, I'm encountering an SSL certificate verification error when attempting to authenticate with ForgeRock (or calling /auth/login endpoint):
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:997)
FastAPI is running in a container, here is my Dockerfile:
FROM xxx.com/xxx/python3.10.9-slim:1.0
WORKDIR /app
COPY ./ /app
COPY ./certs /etc/ssl/certs/ca-certificates
RUN chmod 644 /etc/ssl/certs/ca-certificates/*
RUN apt-get update && apt-get install -y ca-certificates
RUN update-ca-certificates
...
The certificates are moved properly to /etc/ssl/certs/ca-certificates.crt
Here is my simplified FastAPI code:
CA_BUNDLE_PATH = '/etc/ssl/certs/ca-certificates.crt'
os.environ['REQUESTS_CA_BUNDLE'] = CA_BUNDLE_PATH
ssl_context = ssl.create_default_context(cafile=CA_BUNDLE_PATH)
logging.debug(f"SSL context CA certificates: {ssl_context.get_ca_certs()}")
class CustomHttpxClient(httpx.AsyncClient):
def __init__(self, *args, **kwargs):
kwargs['verify'] = ssl_context
super().__init__(*args, **kwargs)
oauth = OAuth()
oauth.register(
name='forgerock',
client_id='client-id',
client_secret='secret',
server_metadata_url='https://forgerock-login',
client_kwargs={
'scope': 'openid profile email',
'httpx_client': CustomHttpxClient()
}
)
async def debug_session_state(request: Request):
session = request.session
state = session.get('state')
logging.debug(f"Session state: {state}")
@app.get('/auth/login')
async def login(request: Request):
try:
redirect_uri = request.url_for('auth_callback')
response = await oauth.forgerock.authorize_redirect(request, redirect_uri)
request.session['state'] = response.state
logging.debug(f"Stored state in session: {request.session['state']}")
return response
except Exception as e:
logging.error(f"Error during authorization redirect: {e}")
raise HTTPException(status_code=500, detail="Internal Server Error")
@app.get('/auth/callback')
async def auth_callback(request: Request):
await debug_session_state(request)
try:
state_in_session = request.session.get('state')
state_in_request = request.query_params.get('state')
logging.debug(f"State in session: {state_in_session}, State in request: {state_in_request}")
if state_in_session != state_in_request:
raise HTTPException(status_code=400, detail="CSRF Warning! State not equal in request and response.")
token = await oauth.forgerock.authorize_access_token(request)
user = token.get('userinfo')
return {'user': user}
except Exception as e:
logging.error(f"Callback Error: {e}")
raise HTTPException(status_code=400, detail="Authentication Failed")
So I loaded CA certificates into a custom SSL context and used it with the httpx.AsyncClient. Anyway, I see this in debug logs:
DEBUG:httpx:load_verify_locations cafile='/usr/local/lib/python3.10/site-packages/certif/cacert.pem'
It seems that SSL context is different than I specified. I tried different methods to pass SSL context, but with no result. Why I can't set my SSL context?
The error message mentions httpx
, not requests
.
httpx
has its own set of Env variables, in this case SSL_CERT_FILE
Hopefully that env var will override the hard-coded CACERT from certify
that is a major pain when you try to use Python in a corporate IS.
[edit] CAVEAT - the httpx
docs now state that you cannot use env vars to override that certifi
package.
Reference: https://www.python-httpx.org/advanced/ssl/#working-with-ssl_cert_file-and-ssl_cert_dir (as of V0.28 docs)