This repository contains the configuration for the Curity Identity Server and nginx to enable Dynamic Client Registration using mTLS. It also shows how to perform a validation of a software statement within nginx. This setup allows to implement compliance with various Open Banking specifications such as the Open Banking Brazil Security Profile. Check out the Open Banking Brazil DCR Request Validation in Nginx for a description of the code example.
We recommend hosting a reverse proxy in front of the Curity Identity Server, both because it is more secure, and also for extensibility reasons. An API gateway or a reverse proxy is not just a simple security product but provides more advanced options that complement the features of the Curity Identity Server. For example, scripts and calls to external services can be used to implement advanced request validation within the gateway. This repo provides a real world example of solving a complex problem that is the implementation of the Open Banking Brazil Dynamic Client Registration Profile. The challenge is outlined in the blogpost Open Banking Brazil Status Update and Short-Term Roadmap.
The following illustration visualizes the approach.
The gateway receives the DCR request, does some preprocessing and, if successful, calls the DCR Validation Module. The module in turn validates the software_statement
via an external service and compares the rest of the DCR request with the content in the statement. In case the DCR request contains data that does not match the content in the software statement, it throws an error. If necessary, the gateway updates the DCR request with default values from the software statement before it forwards the request to the Curity Identity Server. Read more in detail about the approach in Open Banking Brazil DCR Request Validation.
Get a license for the Curity Identity Server from the developer portal at https://developer.curity.io. Copy the license file to idsvr/license.json
.
Run docker compose up nginx
. It will setup an admin node of the Curity Identity Server and a runtime node that is secured by a reverse proxy, i.e. nginx.
The system comes with its own PKI, one for securing the infrastructure ("CA") and one for authentication ("Regulatory CA"). In a production system you will use one PKI such as Let's Encrypt for the server certificates and another one for client and user authentication. For example, in case of the Open Banking Brasil Security Profile client certificates and software statements must be issued by Brazil ICP.
Curity Identity Server is configured to allow dynamic client registration requests by any client that can provide a valid client certificate issued by the Regulatory CA. Nginx validates the client requests and among others checks that the request contains a valid software statement signed by the Regulatory CA. Nginx does not validate the signature of the software statement itself but forwards the token to an external service for validation. To test and run this example make sure you also run the JWT validation service example or update the dcr_request.lua
to point to a different service.
This example includes two certificates: One for the server (nginx) and one that can be used for the client mutual TLS authentication. Like mentioned above they are part of different PKIs. The server certificate is issued by "C=SE, O=Curity AB, OU=Test, CN=Issuing CA" whereas the client certificate for the testclient is issued by "C=BR, O=Open Banking Brasil, OU=Test, CN=Issuing CA". Both CA's are made up.
certificate file | PKI | purpose |
---|---|---|
certs/server-cert-chain.pem | infrastructure | List of trusted issuers of server certificates; required for sending test requests |
certs/testclient.cert.pem | regulatory | Client certificate of the testclient; required for mTLS authentication when sending test requests |
certs/testclient.key.pem | regulatory | Corresponding private key for the client certificate; required for mTLS to work |
gateway/nginx/ssl/trusted-client-cert-issuers.pem | regulatory | List of trusted issuers of client certificates; required for enabling mTLS with certificates signed by the regulatory CA |
gateway/nginx/ssl/server.cert.pem | infrastructure | Server certificate for nginx; required for securing the endpoint with HTTPS (TLS) |
gateway/nginx/ssl/server.key.pem | infrastructure | Corresponding private key for the server certificate; required for HTTPS to work |
The server certificates are loaded in the nginx.conf
:
# SSL settings
ssl_certificate /tmp/server.cert.pem;
ssl_certificate_key /tmp/server.key.pem;
Mutual TLS is enabled by setting the trusted client certificate issuers in nginx.conf
:
ssl_client_certificate /tmp/trusted-client-cert-issuers.pem;
ssl_verify_client on;
If you want to change the SSL certificates in this example, update the COPY
directives in gateway/nginx/Dockerfile
:
COPY gateway/nginx/ssl/server.cert.pem /tmp/server.cert.pem
COPY gateway/nginx/ssl/server.key.pem /tmp/server.key.pem
For different client certificates change the following line in gateway/ngin/Dockerfile
:
COPY gateway/nginx/ssl/trusted-client-cert-issuers.pem /tmp/trusted-client-cert-issuers.pem
Do not forget to update the client trust store at the Curity Identity Server.
How to create a custom CA or server certificates is out of scope of this example. However, if you need more information about how to request a client certificate that complies with Open Banking Brazil, refer to the Certificate Generation Instructions of the OBB Working Group. Ideally, you will replace trusted-client-cert-issuers.pem
with the certificate chain of Brazil ICP and configure nginx with valid server certificates.
A client can only register with a valid software statement. To retrieve a software statement for testing refer to the JWT validation service example that includes a mocked service for generating a software statement that works with this implementation.
curl http://localhost:8080/softwarestatement
Copy the token into the DCR request:
curl --cert certs/testclient.cert.pem --key certs/testclient.key.pem --cacert certs/server-cert-chain.pem https://localhost/oauth/v2/oauth-dynamic-client-registration -d '{ "token_endpoint_auth_method": "tls_client_auth",
"tls_client_auth_subject_dn": "C=SE, O=Power Bank, OU=Test, CN=testclient", "redirect_uris": ["https://tpp.example.com/"], "scope":"openid", "software_statement":"..."}' -v
The following command sends an invalid request. The client must not include jwks
in the request.
curl --cert certs/testclient.cert.pem --key certs/testclient.key.pem --cacert certs/server-cert-chain.pem https://localhost/oauth/v2/oauth-dynamic-client-registration -d '{"jwks":"my-secret-key-data", "token_endpoint_auth_method": "tls_client_auth",
"tls_client_auth_subject_dn": "C=SE, O=Power Bank, OU=Test, CN=testclient", "redirect_uris": ["https://tpp.example.com/"], "scope":"openid", "software_statement":"..."}' -v
DCR requests using templatized clients are very convenient but they are not suitable for this use case because the configuration does only support client secret authentication for newly created clients which does not comply with common Open Banking requirements. Therefore, enable Dynamic Client Registration for non-templatized clients. The client must use a client certificate signed by a regulatory body when sending the DCR request. However, the request will pass a reverse proxy thus the client authentication method set for DCR must be mutual-tls-by-proxy
with the CA certificate of the regulatory body such as the Brazil ICP. The reverse proxy will use the header X-Client-SSL-Cert
for forwarding the client certificate to the Curity Identity Server.
The client's request passes the reverse proxy, i.e. nginx which terminates the TLS sessions in order to validate and adapt the request before sending it to the upstream server aka the Curity Identity Server. As a result nginx handles any mutual TLS session and validates the client's certificate. Only if the certificate is valid, nginx will continue with processing of the request content. To validate the software_statement
in the DCR request an external service is called. This is because of a limitation in the resty.jwt module regarding signature algorithms. After successful validation nginx will add default values before it forwards the request to the Curity Identity Server together with the client certificate in the header X-Client-SSL-Cert
.
...
# Trusted issuers for client certificates
ssl_client_certificate /tmp/ca-bank-chain.pem;
...
access_by_lua_block {
...
-- Load request data
ngx.req.read_body()
local http_body_data = ngx.req.get_body_data()
-- Call module that handles the request validation and returns updated client metadata
metadata_valid, return_value = pcall(dcr_request.validate, http_body_data)
...
-- Exit access phase
if metadata_valid == true then
-- Update request body
ngx.req.set_body_data(return_value)
ngx.log(ngx.DEBUG, "Request Body: " .. return_value)
ngx.exit(ngx.OK)
else
-- Return Error
...
end
}
# Forward request
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Add client certificate
proxy_set_header X-Client-SSL-Cert $ssl_client_escaped_cert;
proxy_pass http://internal-curity-runtime:8443;
Please visit curity.io for more information about the Curity Identity Server.
Check out the Open Banking Brazil Status Update and Short-Term Roadmap.
Also have a look at how the Curity Identity Server supports Open Banking Brazil
Copyright (C) 2021 Curity AB.