How should you host your app in 2024?
All posts

Multi client authentication with auth0 and oauth2-proxy

A simple guide for multi-client authentication with auth0 and oauth2-proxy
January 22, 2024

Implementing auth can be difficult and time consuming, as well as being a critical part of most software systems. This holds especially true for applications that are public/customer facing.

Authentication providers like Auth0 and Okta have become commonplace in software development. These providers help take this work off of your plate, and this can be made even easier by using a reverse proxy that provides authentication capabilities, like oauth2-proxy.

These solutions are fairly straightforward for most applications (API, SPA, etc.) but things start to get complicated when you want to use multiple authentication flows for the same software application/platform.

We'll look at a specific use-case, with the hope that this can be adapted to fit most cases.

Context

  • we're using Auth0 as the authentication provider
  • we have an application (backend API + SPA) that is already set up to use Auth0 along with oauth2-proxy

Objectives

  • we're looking to add a customer-facing cli application
  • the cli will utilize the same backend api that is already in place
  • we want to allow users to authenticate with the cli using the device code flow

Hurdles

  • depending on your implementation, the Auth0 application/client type you use might be a single page web app, a regular web app, or a machine to machine application. None of these support the device code flow, and changing/replacing the current client is not a viable option (bad practice, unsafe, won't work anyway etc.). This means we'll need a 2nd Auth0 application and the type will need to be "native" to support the device code flow.
  • the oauth2-proxy does not support using multiple clients with the same proxy instance

Because of these hurdles, it seemed like we'd no longer be able to use the oauth2-proxy. A custom solution would need to be written. This was saddening as the oauth2-proxy really did make implementing auth a lot easier, and it removed quite a bit of common boiler-plate logic.

I wasn't ready to throw in the towel just yet and this solution, like many of my all-time favorites, was born of a combination of stubbornness and laziness (and a bit of determination).

Solution

Using oauth2-proxy, the original setup looked something like this:


# Original proxy (API application)
oauth2-proxy --provider oidc --provider-display-name "Auth0" \
--client-id="${AUTH0_CLIENT_ID}" \
--cookie-secret="${AUTH0_COOKIE_SECRET}" \
--client-secret="${AUTH0_CLIENT_SECRET}" \
--cookie-expire="${COOKIE_EXPIRE_TIME}" \
--oidc-issuer-url="https://${AUTH0_DOMAIN}/" \
--redirect-url="/api/auth/callback" \
--http-address="http://0.0.0.0:${PORT}" \
--upstream="http://localhost:${API_PORT}" \
--email-domain=* \
--skip-provider-button \
--skip-auth-route="/public/*" \
--redis-connection-url="redis://${REDIS_IP}:${REDIS_PORT}" \
--redis-connection-idle-timeout="5" \
--proxy-prefix="/api/auth" \
--cookie-name="my_session" \
--session-store-type="redis" \
--whitelist-domain="${AUTH0_DOMAIN}" \
--insecure-oidc-allow-unverified-email \
--request-logging="false" \
--pass-authorization-header="true"

We can't add a 2nd client to the proxy, so my intention was to add a 2nd proxy. The first hurdle here was that I wasn't sure how to handle routing to the 2 proxies. I eventually settled on the idea of having the first proxy allow requests to pass through to the 2nd proxy:


CLI_API_PREFIX=/api/cli

# Original proxy (API application)
oauth2-proxy --provider oidc --provider-display-name "Auth0" \
--client-id="${AUTH0_CLIENT_ID}" \
--cookie-secret="${AUTH0_COOKIE_SECRET}" \
--client-secret="${AUTH0_CLIENT_SECRET}" \
--cookie-expire="${COOKIE_EXPIRE_TIME}" \
--oidc-issuer-url="https://${AUTH0_DOMAIN}/" \
--redirect-url="/api/auth/callback" \
--http-address="http://0.0.0.0:${PORT}" \
# Here we add an upstream for the 2nd proxy
--upstream="http://localhost:$(expr $PORT + 1)${CLI_API_PREFIX}/" \
--upstream="http://localhost:${API_PORT}" \
--email-domain=* \
--skip-provider-button \
--skip-auth-route="/public/*" \
# Skip auth for the cli route, the 2nd proxy will handle this
--skip-auth-route="${CLI_API_PREFIX}/*" \
# Without the --skip-jwt.. and --oidc-extra-audience flags, the
# request is stripped of information needed to authenticate with the
# 2nd proxy (headers, token)
--skip-jwt-bearer-tokens="true" \
--oidc-extra-audience="${AUTH0_CLI_CLIENT_ID}" \
--redis-connection-url="redis://${REDIS_IP}:${REDIS_PORT}" \
--redis-connection-idle-timeout="5" \
--proxy-prefix="/api/auth" \
--cookie-name="my_session" \
--session-store-type="redis" \
--whitelist-domain="${AUTH0_DOMAIN}" \
--insecure-oidc-allow-unverified-email \
--request-logging="false" \
--pass-authorization-header="true" \
&

# Start the 2nd proxy (for the CLI application)
oauth2-proxy --provider oidc --provider-display-name "Auth0" \
--client-id="${AUTH0_CLI_CLIENT_ID}" \
# This --cookie-* attr doesn't really matter here (I believe it just couldn't be blank)
--cookie-secret="${SOME_AUTH0_COOKIE_SECRET}" \
--client-secret="${AUTH0_CLI_CLIENT_SECRET}" \
--oidc-issuer-url="https://${AUTH0_DOMAIN}/" \
--redirect-url="${CLI_API_PREFIX}/auth/callback" \
--http-address="http://0.0.0.0:$(expr $PORT + 1)" \
# Same upstream as the 1st proxy since we're using the same backend API
--upstream="http://localhost:${API_PORT}" \
--email-domain=* \
--skip-provider-button \
--skip-jwt-bearer-tokens="true" \
--redis-connection-url="redis://${REDIS_IP}:${REDIS_PORT}" \
--redis-connection-idle-timeout="5" \
--proxy-prefix="${CLI_API_PREFIX}/auth" \
--cookie-name="my_cli_session" \
--session-store-type="redis" \
--whitelist-domain="${AUTH0_DOMAIN}" \
--insecure-oidc-allow-unverified-email \
--request-logging="false" \
--pass-authorization-header="true" \
&

N.B. If the additional audiences are not added, then the 1st proxy will strip all auth info (headers, etc.) from the request before passing it to the 2nd proxy. This doesn't make the 1st proxy treat the request as if it's authenticated, it just allows the request to make it to the 2nd proxy without being stripped.It took an embarrassing amount of time for me to figure this out, don't be like me :).

This solution worked perfectly for my use case, and I hope it helps with yours as well. If there's anything missing here, or if you have any feedback/questions feel free to reach out to adam.abdelaziz@withcoherence.com. Happy coding!