Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example: Logout implementation with Google Provider and NGINX #543

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions examples/google-logout/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Simple Logout Solution with the Google Provider, NGINX, and Python

If you're using the [Google Identity](https://developers.google.com/identity) IdP on your app, you'll likely want to implement some sort of logout button as well. In Oauth speak, this means submitting the Google **access token** to Google's **revocation endpoint**, then clearing the Vouch session token.

## A note on tokens

There are several auth tokens used in the Vouch auth flow, and it's easy to get them mixed up.

- **Google Identity Token**: A signed [JWT](https://jwt.io/) with identity information about the user.
- **Google Access Token**: An opaque token which can be used to access Google APIs on behalf of the user. Vouch uses the user's **access token** to call Google's [OpenID Connect](https://developers.google.com/identity/protocols/oauth2/scopes#openid-connect) API, which serves the user's **identity token**.
- **Vouch Session Token**: A JWT-formatted session cookie which allows Vouch to continue to authenticate signed-in users without making repeated calls to Google's APIs.

## Implementation

### Revoking the access token

We can revoke the Google **Access Token** by invoking Google's [revocation endpoint](https://developers.google.com/identity/protocols/oauth2/web-server#tokenrevoke). Let's create a simple Python function that accepts an access token and submits it to the revocation endpoint.

```python
import requests

def revoke(token: str) -> requests.Response:
return requests.post('https://oauth2.googleapis.com/revoke',
params={'token': token},
headers = {'content-type': 'application/x-www-form-urlencoded'})
```

Let's then wrap this function in a simple HTTP server so that we can call it from NGINX. We'll pass the **access token** via a custom `X-Access-Token` header.

```python
import requests
from http.server import BaseHTTPRequestHandler, HTTPServer

# Define HTTP server
class S(BaseHTTPRequestHandler):
# Send access token to Google's revocation endpoint
def _revoke(self, token: str) -> requests.Response:
return requests.post('https://oauth2.googleapis.com/revoke',
params={'token': token},
headers = {'content-type': 'application/x-www-form-urlencoded'})

def do_GET(self):
token = self.headers.get("X-Ems-Access-Token")
revoke_response = self._revoke(token)
pass

# Startup server and set shutdown conditions
def run(server_class=HTTPServer, handler_class=S, port=8080):
server_address = ('', port)
httpd = server_class(server_address, handler_class)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
```

### Invalidating the session token

The `do_GET` function calls the `_revoke` function, which sends the user's access token to the Google revocation endpoint. This is good, but we still need to invalidate the user's Vouch **session cookie**. We do this by redirecting the user to `https://vouch.xxxxxxxxxxxxxxxx.com/logout`.

The `/logout` endpoint accepts a single URL parameter: `url`, which specifies where the user should be redirected after being signed off. Let's set the `Location` header and status code of our `do_GET` function to redirect our users.

```python
# ...
def do_GET(self):
token = self.headers.get("X-Ems-Access-Token")
revoke_response = self._revoke(token)
self.send_response(302)
self.send_header("Location", f"https://vouch.xxxxxxxxxxxxxxxx.xxx/logout?url={redirect_url}")
self.end_headers()
return
```

All we need to do now is run our `run` function!

```python
# ...
if __name__ == '__main__':
from sys import argv

if len(argv) == 1:
# Set default port to 8080.
port = 8080
else:
port=int(argv[1])

run(port=port)
```

For a complete implementation with error handling and logging, check out the `logout.py` file.

## Integrating Our Logout Server

To integrate our logout server, we'll first need to run our Python script with a daemon. I prefer **systemd** on Ubuntu. Run the following commands to get that going.

This assumes you already have Python3 installed.

```bash
# Make a logout_py user for our daemon to run as
# Ignore the "missing or non-executable shell" warning
sudo useradd -M -s /bin/nologin logout_py

# Make a server directory
sudo mkdir -p /opt/logout_py
sudo chown logout_py:logout_py /opt/logout_py

# Set up virtual environment
sudo python3 -m venv /opt/logout_py/venv
sudo su -c "source /opt/logout_py/venv/bin/activate && pip install requests"

# Move script into position
sudo cp ./logout.py /opt/logout_py/logout.py

# Move systemd service definition into position
# Make sure you edit the `PORT` variable as necessary!
sudo cp ./logout_py.service /etc/systemd/system/logout_py.service

# Start logout.py daemon
sudo systemctl daemon-reload
sudo systemctl start logout_py.service
sudo systemctl status logout_py.service
```

With that set up, we then need to create a `/logout` location in our NGINX server that forwards the appropriate headers to our **logout.py** server. See `./auth_server.conf` for variable declarations for $sub, $access_token, and $http_x_forwarded_host.

```
location /logout {
proxy_pass http://127.0.0.1:8080;

proxy_set_header X-Access-Token $access_token;

# For logging
proxy_set_header X-Google-Sub $sub;

# For redirection
proxy_set_header X-Forwarded-Host $http_host;
# You may need to forward the host if using nested proxies
# proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
}
```

And that's it! All you need to do on the frontend is to link to `/logout`. After a quick refresh, you should see that your users need to sign in again before accessing the site.
48 changes: 48 additions & 0 deletions examples/google-logout/auth_server.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# NGINX server declaration
server {
server_name xxx.xxxxxxxxxxxxxxxx.xxx/;
listen 443;

# ---------SSL Cert Stuff Goes Here---------------

auth_request /validate;

# We need to pass the access token to our logout server
# Make sure that Vouch's config.yml includes the declaration
# vouch.headers.accesstoken: X-Vouch-IdP-AccessToken
auth_request_set $access_token $upstream_http_x_vouch_idp_accesstoken;
auth_request_set $sub $upstream_http_x_vouch_idp_claims_sub;

location = /validate {
proxy_pass http://vouch_backend/validate;
proxy_set_header Host $http_x_forwarded_host;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
auth_request_set $auth_resp_jwt $upstream_http_x_vouch_jwt;
auth_request_set $auth_resp_err $upstream_http_x_vouch_err;
auth_request_set $auth_resp_failcount $upstream_http_x_vouch_failcount;
}
error_page 401 = @error401;

location @error401 {
return 302 https://vouch.xxxxxxxxxxxxxxxxxxxxxxxx.xxx/login?url=https://$http_host$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err;
}

# ---------------------Google Token Revocation---------------------
location /logout {
proxy_pass http://127.0.0.1:8080;

proxy_set_header X-Access-Token $access_token;

# For logging
proxy_set_header X-Google-Sub $sub;

# For redirection
# You may need to forward the host if using nested proxies
proxy_set_header X-Forwarded-Host $http_host;
# proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
}
# =====================END AUTHENTICATION=====================
}


30 changes: 30 additions & 0 deletions examples/google-logout/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
vouch:
domains:
- xxxxxxxxxxxxxxxxxxxxxxxx.xxx

headers:
# We need this token to pass along to our logout server.
# This unfortunately makes our Vouch cookie big, but we need it.
accesstoken: X-Vouch-IdP-AccessToken

claims:
- sub

cookie:
secure: true

post_logout_redirect_uris:
- "https://xxx.xxxxxxxxxxxx.xxx/"

oauth:
provider: google
client_id: xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
client_secret: xxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxx
callback_urls:
- https://vouch.xxxxxxxxxxxxxxxxxxxxx.xxx/auth
preferredDomain: xxxxxxxxxxxxxxxxxxxxxxxx.xxx

scopes:
- openid
- profile
- email
73 changes: 73 additions & 0 deletions examples/google-logout/logout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import logging
import requests

from http.server import BaseHTTPRequestHandler, HTTPServer

class S(BaseHTTPRequestHandler):
# Send access token to Google's revocation endpoint
def _revoke(self, token: str) -> requests.Response:
return requests.post('https://oauth2.googleapis.com/revoke',
params={'token': token},
headers = {'content-type': 'application/x-www-form-urlencoded'})

def do_GET(self):
token = self.headers.get("X-Access-Token")
sub = self.headers.get("X-Google-Sub")
host = self.headers.get("X-Forwarded-Host")

if not host:
host = self.headers.get("Host")
host = host.split('/')[0]

if not sub:
sub = "Unknown User"

if not token:

logging.warning("No token header supplied.")
self.send_response(400)
self.send_header("Content-Type", "text/html")
self.end_headers()

self.wfile.write(f'<p>Whoops! Something went wrong. <a href="https://{host}/">Return home</a><p>'.encode())
return

revoke_response = self._revoke(token)
if revoke_response.status_code >= 200 and revoke_response.status_code <= 299:
logging.info(f"Revoked access token {sub}")
else:
logging.warning(f"Failed to revoke access token {sub}")

self.send_response(302)
self.send_header("Location", f"https://vouch.enrollmentmanagementservices.com/logout?url=https://{host}/")
self.end_headers()
return

def run(server_class=HTTPServer, handler_class=S, port=8020):
logging.info(f"Attempting to serve logout.py on port {port}")
server_address = ('', port)
httpd = server_class(server_address, handler_class)
logging.info(f'Serving on port {port}')
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
logging.info('Stopping logout.py\n')

if __name__ == '__main__':
from sys import argv

#logging.basicConfig(level=logging.INFO)

logging.basicConfig(level=logging.INFO)
logging.info("Starting Logout.py")

if len(argv) == 1:
logging.warning("Port not specified. Starting on the default 8080.")
logging.warning("To choose a different port, rerun this script with the desired port as the first CLI argument.")
port = 8080
else:
port=int(argv[1])

run(port=port)
13 changes: 13 additions & 0 deletions examples/google-logout/logout_py.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[Unit]
Description=Logout.py
After=network.target

[Service]
User=logout_py
Environment="PORT=8080"
ExecStart=/opt/logout_py/venv/bin/python3 /opt/logout_py/logout.py ${PORT}
RestartSec=5
Restart=on-success

[Install]
WantedBy=multi-user.target