NGINX/OpenResty Reverse Proxy versus Identity Provider integrations

Boris Figovsky
3 min readMar 7, 2022

A common practice to separate web servers from their users is to use a reverse proxy. For most backend applications, this is mostly a trivial task. However, this is less trivial for applications that integrate with Identity Providers. In this blog post, we configure NGINX and OpenResty as reverse proxies.

Photo by İsmail Enes Ayhan on Unsplash

Introduction

An Identity Provider is a service that provides user identities to a web application. Known Identity Providers include:

The Identity Provider is responsible for authenticating users and storing their passwords securely. An over-simplified user authentication happy flow is as follows:

  1. A user browses to a web application;
  2. The application responds with an HTTP Temporary Redirect (or equivalent) to an Identity Provider. The application encodes the required information in the request so that the Identity Provider can redirect the user back to the application;
  3. The user sees a sign-in form and enters credentials;
  4. If the credentials are correct, the Identity Provider responds with another HTTP Temporary Redirect to the application with the user token. Otherwise — with an error.

For the sake of the discussion, let’s assume that:

  • We have deployed the application at https://my.internal.app ;
  • We will deploy the reverse proxy at https://my.app ;
  • Only the reverse proxy can reach the application;
  • The application is not aware of the reverse proxy;
  • The identity provider is at https://idp ;
  • The identity provider is aware of the hostname of the reverse proxy.

With these assumptions, step #2 above becomes a challenge: it contains the URL of the application, and the reverse proxy must patch it.

OAuth v2.0

If the Identity Provider is OAuth v2.0 based (such as Google), the temporary redirect location of step #2 above looks like:

https://idp/oauth2/authorize?....&redirect_uri=https://my.internal.app/handle/auth/response&...

For the Identity Provider to accept the parameters and be able to redirect back, the reverse proxy must fix the above URL to:

https://idp/oauth2/authorize?....&redirect_uri=https://my.app/handle/auth/response&...

With NGINX as a reverse proxy, the server block should contain:

server {
listen 443 ssl;
server_name my.app;
# TLS configurations should be here
set $upstream my.internal.app;
location / {
proxy_pass https://$upstream:443;
proxy_redirect https://my.internal.app/ https://my.app/;
}
}

The reverse proxy will be able to patch redirects to the Identity Provider.

SAML V2.0

However, if the Identity Provider is SAML V2.0 based, step #2 looks differently. It is called Binding in SAML V2.0 terminology. It is one:

  1. HTTP Redirect Binding: Redirect to https://idp/SAML2/SSO/Redirect?SAMLRequest=<request> , where request is a base64-encoded Zlib-compressed <samlp:AuthnRequest> XML;
  2. HTTP POST Binding: Form submission to https://idp/SAML2/SSO/POST with SAMLRequest parameter containing a base64-encoded Zlib-compressed <samlp:AuthnRequest> XML;
  3. HTTP Artifact Binding: Redirect to https://idp/SAML2/SSO/Artifact?SAMLart=<artifact> where <artifact> is a reference to an <samlp:AuthnRequest> XML. The Identity Provider will request it via an HTTP request to the application.

A sample <samlp:AuthnRequest> XML looks like this:

<saml2p:AuthnRequest
xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
AssertionConsumerServiceURL="https://my.app/saml2/idpresponse"
Destination="https://idp/saml2"
ID="<some-id>"
IssueInstant="<timestamp>"
Version="2.0">
<saml2:Issuer
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
>my-app-saml-identity-id</saml2:Issuer>
</saml2p:AuthnRequest>

For example, to patch an HTTP Redirect or POST Binding, the reverse proxy must:

  1. Extract the SAMLRequest parameter from the Location HTTP header or Body;
  2. Base64-decode it;
  3. Decompress the Base64-decoded result to get the AuthnRequest XML;
  4. Patch the Assertion Consumer Service URL;
  5. Re-compress the patched XML;
  6. Base64-encode the compressed result;
  7. Replace the SAMLRequest parameter with the new base64 text.

NGINX’s proxy_redirect configuration cannot do this.

OpenResty to the Rescue!

By replacing NGINX with OpenResty, we can achieve our goal. OpenResty is a packaged NGINX with Lua support. OpenResty can invoke Lua scripts and code at different hook points, such as intercepting HTTP responses and altering them.

For example, to fix an HTTP Redirect Binding, we use the following OpenResty configuration in the server block:

server {
listen 443 ssl;
server_name my.app;
# TLS configurations should be here
set $upstream my.internal.app;
location / {
proxy_pass https://$upstream:443;
header_filter_by_lua_file reverse_proxy_http_redirect_binding.lua;
}
}

with an additional Lua script that implements the required logic. The script does nothing if a SAML Request is not detected.

Conclusion

NGINX and OpenResty are good candidates for implementing a reverse proxy for applications. When integrating with an Identity Provider, special considerations are in order. The presented configuration handles it seamlessly.

--

--