Search code examples
ssloauth-2.0fastapihttpxforgerock

FastAPI OAuth2 with ForgeRock: SSL Certificate Verification Issue


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?


Solution

  • 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.

    Reference: https://www.python-httpx.org/environment_variables/