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

NGINX Redirection #1

Merged
merged 1 commit into from
Jan 31, 2024
Merged
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
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
Loading