OAuth2 Proxy is a small Go service that sits next to your reverse proxy and makes users sign in with an OAuth2 or OpenID Connect provider (GitHub, Google, Keycloak and others) before a request reaches your application. The application itself needs no changes. In this tutorial you will install OAuth2 Proxy on Ubuntu 24.04, connect it to GitHub, and use Nginx's auth_request module so only members of your GitHub organization can open https://app.example.com. You will also see how to switch to Google or a generic OIDC provider such as Keycloak.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root
sudouser. - Nginx installed, and a DNS A record for
app.example.compointing to the server. Replaceapp.example.comwith your own hostname throughout. - The application you want to protect, listening on
127.0.0.1:3000. If you do not have one yet, the guide starts a test page for you in Step 5. - A GitHub account that belongs to the organization you want to allow (or a Google Cloud or Keycloak account if you use those providers).
- Ports 80 and 443 open to the internet.
Step 1 - Installing OAuth2 Proxy
OAuth2 Proxy is distributed as a static binary on its GitHub releases page. Check the latest release and set the version accordingly:
VERSION=7.12.0
cd /tmp
curl -fLO "https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v${VERSION}/oauth2-proxy-v${VERSION}.linux-amd64.tar.gz"
tar -xzf "oauth2-proxy-v${VERSION}.linux-amd64.tar.gz"
sudo install -m 0755 "oauth2-proxy-v${VERSION}.linux-amd64/oauth2-proxy" /usr/local/bin/oauth2-proxy
On an ARM server, replace linux-amd64 with linux-arm64. Confirm the binary runs:
oauth2-proxy --version
oauth2-proxy v7.12.0 (built with go1.24.x)
Create a system user without a shell and a configuration directory:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin oauth2-proxy
sudo mkdir -p /etc/oauth2-proxy
Step 2 - Registering a GitHub OAuth app
OAuth2 Proxy needs a client ID and secret from the provider. In GitHub, open your organization's Settings > Developer settings > OAuth Apps > New OAuth App (or your personal Settings > Developer settings if you prefer) and fill in:
- Application name: a name users will recognize, for example
Internal tools. - Homepage URL:
https://app.example.com - Authorization callback URL:
https://app.example.com/oauth2/callback
Click Register application, then Generate a new client secret. Copy the Client ID and the secret; GitHub shows the secret only once.
Step 3 - Writing the configuration
OAuth2 Proxy encrypts its session cookie with a secret that must be 16, 24 or 32 bytes. Generate a 32-byte value:
openssl rand -base64 32 | tr -- '+/' '-_'
rT1q7m1Yb2kQf8oJ0cX9vN3sL5hP4wE6aZ2uD7gH1iM=
Create the configuration file:
sudo nano /etc/oauth2-proxy/oauth2-proxy.cfg
# Provider
provider = "github"
client_id = "your_github_client_id"
client_secret = "your_github_client_secret"
redirect_url = "https://app.example.com/oauth2/callback"
# Who is allowed in
github_org = "your-github-org"
email_domains = ["*"]
# Listen only on localhost; Nginx talks to it
http_address = "127.0.0.1:4180"
reverse_proxy = true
# Nginx auth_request mode: answer 202 instead of proxying,
# and return the user identity in X-Auth-Request-* headers
upstreams = ["static://202"]
set_xauthrequest = true
# Session cookie
cookie_secret = "your_cookie_secret"
cookie_secure = true
cookie_samesite = "lax"
cookie_expire = "168h"
Replace the placeholders with your client ID, client secret, GitHub organization name and the cookie secret you generated. A few notes:
github_orglimits access to members of that organization. Addgithub_team = "your-team"to narrow it to one team.email_domainsmust still be set;["*"]means the organization check is the only filter.upstreams = ["static://202"]is the standard pattern for Nginxauth_request: Nginx asks OAuth2 Proxy whether a request is authenticated and then proxies it to the application itself.reverse_proxy = truemakes OAuth2 Proxy trust theX-Forwarded-*headers from Nginx.
The file contains secrets, so make it readable only by the service user:
sudo chown root:oauth2-proxy /etc/oauth2-proxy/oauth2-proxy.cfg
sudo chmod 640 /etc/oauth2-proxy/oauth2-proxy.cfg
Step 4 - Running OAuth2 Proxy as a systemd service
Create a unit file:
sudo nano /etc/systemd/system/oauth2-proxy.service
[Unit]
Description=OAuth2 Proxy
After=network-online.target
Wants=network-online.target
[Service]
User=oauth2-proxy
Group=oauth2-proxy
ExecStart=/usr/local/bin/oauth2-proxy --config=/etc/oauth2-proxy/oauth2-proxy.cfg
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Start it and enable it at boot:
sudo systemctl daemon-reload
sudo systemctl enable --now oauth2-proxy
Check that it is running and answering on its health endpoint:
sudo systemctl status oauth2-proxy --no-pager
curl -s http://127.0.0.1:4180/ping; echo
● oauth2-proxy.service - OAuth2 Proxy
Active: active (running) since Thu 2026-09-24 11:02:17 UTC; 4s ago
OK
If it exits immediately, sudo journalctl -u oauth2-proxy -n 30 shows the configuration error, for example an invalid cookie secret length.
Step 5 - Starting a test application
If your real application is already on 127.0.0.1:3000, skip this step. Otherwise start a throwaway web server in a second terminal so you have something to protect:
mkdir -p ~/testapp && echo "Hello from the protected app" > ~/testapp/index.html
python3 -m http.server 3000 --bind 127.0.0.1 --directory ~/testapp
Leave it running until you finish testing.
Step 6 - Configuring Nginx with auth_request
Nginx on Ubuntu includes the auth_request module. The server block needs three locations: /oauth2/ for the sign-in and callback pages, /oauth2/auth for the per-request check, and / for the application:
sudo nano /etc/nginx/sites-available/app.example.com
server {
listen 80;
listen [::]:80;
server_name app.example.com;
# Sign-in, callback and sign-out pages served by OAuth2 Proxy
location /oauth2/ {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Auth-Request-Redirect $request_uri;
}
# Subrequest used by auth_request on every request
location = /oauth2/auth {
internal;
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
location / {
auth_request /oauth2/auth;
error_page 401 =403 /oauth2/sign_in;
# Pass the authenticated identity to the application
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $email $upstream_http_x_auth_request_email;
proxy_set_header X-User $user;
proxy_set_header X-Email $email;
# Forward refreshed session cookies to the browser
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support for apps that need it
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}
}
Enable the site, test the syntax and reload:
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Session cookies are marked secure, so the site must be served over HTTPS. Install Certbot and let it add TLS to the server block:
sudo apt install certbot python3-certbot-nginx
sudo ufw allow 'Nginx Full'
sudo certbot --nginx -d app.example.com --redirect
Step 7 - Testing the login flow
An unauthenticated request must not reach the application. Nginx should return the OAuth2 Proxy sign-in page with status 403:
curl -s -o /dev/null -w "%{http_code}\n" https://app.example.com/
403
Now open https://app.example.com/ in a browser. You see a Sign in with GitHub button. After you authorize the app on GitHub, you are sent back to the application and see Hello from the protected app. The service log records the login:
sudo journalctl -u oauth2-proxy -n 5 --no-pager
... [AuthSuccess] Authenticated via OAuth2: Session{email:[email protected] user:jsmith ...}
A GitHub account outside the organization is rejected with a 403 page. To sign out, visit https://app.example.com/oauth2/sign_out.
When you are done testing, stop the Python test server with Ctrl+C and point proxy_pass in the / location at your real application.
Step 8 - Using Google or an OIDC provider instead
Only the provider block of the configuration changes. After editing /etc/oauth2-proxy/oauth2-proxy.cfg, run sudo systemctl restart oauth2-proxy.
In the Google Cloud console, create an OAuth client ID of type Web application under APIs & Services > Credentials, with https://app.example.com/oauth2/callback as an authorized redirect URI. Then replace the provider settings:
provider = "google"
client_id = "your_client_id.apps.googleusercontent.com"
client_secret = "your_google_client_secret"
redirect_url = "https://app.example.com/oauth2/callback"
email_domains = ["example.com"]
Remove github_org. Here email_domains does the filtering: only accounts with an @example.com address get in.
Keycloak or any OIDC provider
Create a confidential OIDC client in your identity provider with https://app.example.com/oauth2/callback as the redirect URI, then use the generic oidc provider with the issuer URL. OAuth2 Proxy reads the endpoints from the issuer's discovery document:
provider = "oidc"
provider_display_name = "Company SSO"
oidc_issuer_url = "https://auth.example.com/realms/myapp"
client_id = "oauth2-proxy"
client_secret = "your_oidc_client_secret"
redirect_url = "https://app.example.com/oauth2/callback"
email_domains = ["*"]
code_challenge_method = "S256"
cookie_refresh = "1h"
cookie_refresh renews the session with the provider's refresh token every hour, so disabling a user in the identity provider takes effect quickly instead of when the cookie expires.
Allowing specific people
With any provider you can allow a fixed list of addresses instead of a whole domain. Put one address per line in a file and reference it:
authenticated_emails_file = "/etc/oauth2-proxy/allowed-emails.txt"
Troubleshooting
500 or "Unable to find a valid CSRF token" after the callback. The browser did not send back the cookie set before the redirect. Make sure the site is served over HTTPS, the domain in redirect_url matches the one in the address bar, and cookie_secure = true is not used over plain HTTP.
redirect_uri mismatch error on the provider's page. The callback URL registered with GitHub, Google or your OIDC provider must be exactly https://app.example.com/oauth2/callback.
Login succeeds but you get a 403 "Permission Denied" page. The account did not pass the filters. Check github_org, github_team, email_domains or the emails file, and look for Permission Denied in sudo journalctl -u oauth2-proxy.
Nginx returns 500 for every request. The auth_request subrequest cannot reach OAuth2 Proxy. Check curl http://127.0.0.1:4180/ping and /var/log/nginx/error.log.
Conclusion
Your application is now reachable only after a successful login with GitHub, and the same OAuth2 Proxy instance can switch to Google or any OIDC provider by changing a few lines. To protect more apps, add a server block per hostname with the same three locations, or share one login across subdomains with cookie_domains and whitelist_domains. For several OAuth2 Proxy instances behind a load balancer, move sessions to Redis with session_store_type = "redis".
