Skip to content

Commit

Permalink
Merge pull request #1 from als-computing/nginx_redirect
Browse files Browse the repository at this point in the history
NGINX Redirection
  • Loading branch information
rajasriramoju authored Jan 31, 2024
2 parents 10f69c2 + 5041225 commit 207d643
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 70 deletions.
23 changes: 23 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Example
This example provides an environment where you can test splash_auth with and OIDC auth provider of your choice.

This has been tested with podman and podman-compose. It has not been tested with docker.

##Services
### nginx
The services herin use `nginx` to handle proxying and authenticating.

### splash_auth
Provides service side support for the OIDC Code flow

### python_server
A simple python server, demonstrating that you can access it if you're logged in, and not if you're not.



## Setup
1. Edit `/examples/.env`, adding `client_id` and `client_secret` for your provider.
2. Edit `users.yml` and `api_keys.yml` adding what you need.
3. cd in to the `exmaples` directory and type `podman-compose up -d`
4. Browse to localhost:8080

57 changes: 57 additions & 0 deletions example/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
version: "3.3"
services:
python_server:
image: "python:3.11-slim-buster"

expose:
- "8081"
command: "python -m http.server 4200"

nginx:
container_name: nginx
image: nginx
ports:
- 127.0.0.1:8080:80
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
#restart: unless-stopped
logging:
options:
max-size: "1m"
max-file: "3"
networks:
splash_auth_network:

splash_auth:
container_name: splash_auth
#image: ghcr.io/als-computing/splash_auth:main
build:
context: ..
# command: sleep 99999
command: uvicorn splash_auth.main:app --proxy-headers --host 0.0.0.0 --port 8000 --log-level=debug --use-colors --reload
environment:
- OAUTH_AUTH_ENDPOINT=https://accounts.google.com/o/oauth2/v2/auth
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
- OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
- OAUTH_REDIRECT_URI=http://localhost:8080/oidc/auth/code
- OAUTH_TOKEN_URI=https://oauth2.googleapis.com/token
- OUATH_JWKS_URI=https://www.googleapis.com/oauth2/v3/certs
- TOKEN_EXP_TIME=172400
- JWT_SECRET=${JWT_SECRET}
- OUATH_SUCCESS_REDIRECT_URI=http://localhost:8080/
- OUATH_FAIL_REDIRECT_URI=http://localhost:8080
- HTTPX_LOG_LEVEL=trace
volumes:
- ../:/app
- ./users.yml:/app/users.yml
- ./api_keys.yml:/app/api_keys.yml
restart: unless-stopped
logging:
options:
max-size: "1m"
max-file: "3"
networks:
splash_auth_network:
networks:
splash_auth_network:
driver: bridge
94 changes: 94 additions & 0 deletions example/nginx/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;


events {
worker_connections 1024;
}


http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
#tcp_nopush on;

keepalive_timeout 65;

gzip on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

underscores_in_headers on;

server{
listen 80;
keepalive_timeout 70;

# All HTTP traffic will be redirected to to the auth server
location / {
auth_request /oauth2/auth;
error_page 401 = /login;
proxy_pass http://python_server:4200;
proxy_buffer_size 8k;
error_page 401 = /oauth2/sign_in;
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;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Auth-Request-Redirect $request_uri;
auth_request_set $auth_cookie $upstream_http_set_cookie;
}

# This is where the auth_request points, all messages needing auth go to the auth server
# The auth server returns a 200 if the user is authenticated, otherwise a 401
location = /oauth2/auth {
proxy_pass http://splash_auth:8000/oauth2/auth;
proxy_buffer_size 8k;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Content-Length "";
proxy_set_header X-Auth-Request-Redirect $request_uri;
proxy_pass_request_body off;
}


# The login page is unprotected
location /login {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $request_uri;
proxy_buffer_size 8k;
proxy_pass http://splash_auth:8000/login;
}


# For OIDC, the browser is redirected to the auth server to exchange a code
location = /oidc/auth/code {
proxy_pass http://splash_auth:8000/oidc/auth/code;
proxy_buffer_size 8k;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Content-Length "";
proxy_set_header X-Auth-Request-Redirect $request_uri;
proxy_pass_request_body off;
}

}
}
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ dependencies = [

]
dev-dependencies = [
"pytest"
"pytest",
"pre-commit",
"flake8"
]

dynamic = ["version"]
Expand Down
7 changes: 0 additions & 7 deletions splash_auth/_tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
import os




def test_config(monkeypatch):

monkeypatch.setenv("JWT_SECRET", "secret")
Expand Down Expand Up @@ -37,5 +32,3 @@ def test_config(monkeypatch):
assert config.http_client_timeout_all == 1.0
assert config.http_client_timeout_connect == 4.0
assert config.http_client_timeout_pool == 10


4 changes: 2 additions & 2 deletions splash_auth/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
__version_tuple__: VERSION_TUPLE
version_tuple: VERSION_TUPLE

__version__ = version = '0.1.11.dev2+g8d3f690.d20231011'
__version_tuple__ = version_tuple = (0, 1, 11, 'dev2', 'g8d3f690.d20231011')
__version__ = version = '0.1.11.dev3+g10f69c2.d20240131'
__version_tuple__ = version_tuple = (0, 1, 11, 'dev3', 'g10f69c2.d20240131')
2 changes: 1 addition & 1 deletion splash_auth/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


JWT_SECRET = os.environ["JWT_SECRET"]
TOKEN_EXP_TIME = int(os.environ["TOKEN_EXP_TIME"])
TOKEN_EXP_TIME = int(os.environ["TOKEN_EXP_TIME"], )
OAUTH_AUTH_ENDPOINT = os.environ["OAUTH_AUTH_ENDPOINT"]
OAUTH_CLIENT_ID = os.environ["OAUTH_CLIENT_ID"]
OAUTH_CLIENT_SECRET = os.environ["OAUTH_CLIENT_SECRET"]
Expand Down
66 changes: 17 additions & 49 deletions splash_auth/main.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import logging
import os
from enum import Enum
from typing import List, Optional, Union
from typing import Union

import httpx
from fastapi import Cookie, Depends, FastAPI, HTTPException, Request, Response
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.responses import HTMLResponse
from fastapi.security import HTTPBearer
from jose import jwt
from starlette.background import BackgroundTask
from starlette.responses import StreamingResponse
from starlette.status import HTTP_403_FORBIDDEN, HTTP_502_BAD_GATEWAY
from starlette.status import HTTP_401_UNAUTHORIZED

from .config import config
from .oidc import oidc_router
Expand Down Expand Up @@ -94,9 +92,9 @@ async def endpoint_login(redirect: Union[str, None] = None):


@app.api_route(
"/{path:path}", methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS", "HEAD"]
"/oauth2/auth", methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS", "HEAD"]
)
async def endpoint_reverse_proxy(
def auth(
request: Request,
response: Response,
als_token: Union[str, None] = Cookie(default=None),
Expand All @@ -116,65 +114,35 @@ async def endpoint_reverse_proxy(
user is redirected to the /login endpoint, which allows them to login.
"""

logger.info(f"{request.method} - {request.url}")
# check for api key in bearer
if bearer:
if bearer.credentials in users_db.api_keys:
return await _reverse_proxy(request)
response.status_code = 200
return response
else:
logger.debug(f"bearer found, but unknown api_key {bearer.credentials}")
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
)

# check for cookie
if not als_token:
return RedirectResponse("/login")
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
)

# check if cookie's value is valid
try:
jwt.decode(als_token, config.jwt_secret, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
# Signature has expired
logger.debug("Signature expired in cookie")
return RedirectResponse("/login")

response.status_code = 200
try:
return await _reverse_proxy(request)
except Exception:
# a problem exists with the client not accepting new connections
# this is ugly, but we try and keep the service running by killing
# the client and starting fresh
logger.error("Exception from http client", exc_info=1)
global client
await client.aclose()
client = new_httpx_client()
raise HTTPException(
status_code=HTTP_502_BAD_GATEWAY, detail="Excpetion talking to service"
)


async def close(resp: StreamingResponse):
await resp.aclose()


async def _reverse_proxy(
request: Request, scopes: Optional[List[str]] = None
) -> StreamingResponse:
# # cheap and quick scope feature
# if scopes and request.method.lower() in sceope
global client

url = httpx.URL(path=request.url.path, query=request.url.query.encode("utf-8"))
rp_req = client.build_request(
request.method, url, headers=request.headers.raw, content=await request.body()
)
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
)

rp_resp = await client.send(rp_req, stream=True)
return StreamingResponse(
rp_resp.aiter_raw(),
status_code=rp_resp.status_code,
headers=rp_resp.headers,
background=BackgroundTask(close, rp_resp),
)
response.status_code = 200
response.content = "Authentication success"
return response
10 changes: 1 addition & 9 deletions splash_auth/oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,6 @@ async def get_user_info(user_info_url, access_token):
return response.json()


async def get_user_info(user_info_url, access_token):
"""Unused but useful method for getting additional user information"""
response = httpx.get(
url=user_info_url, headers={"Authorization": "Bearer " + access_token}
)
return response.json()


@oidc_router.get("/auth/code")
async def endpoint_validate_ouath_code(request: Request):
"""
Expand Down Expand Up @@ -147,7 +139,7 @@ async def endpoint_validate_ouath_code(request: Request):
encoded_jwt = jwt.encode(
{
"email": id_claims["email"],
"exp": datetime.now(tz=timezone.utc) + timedelta(seconds=config.token_time),
"exp": datetime.now(tz=timezone.utc) + timedelta(seconds=config.token_exp_time),
},
config.jwt_secret,
algorithm="HS256",
Expand Down
2 changes: 1 addition & 1 deletion splash_auth/user_db.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Dict, List
from typing import List

import yaml

Expand Down

0 comments on commit 207d643

Please sign in to comment.