Skip to content

Commit

Permalink
Almost-working example using the mock-oidc-server for test and develo…
Browse files Browse the repository at this point in the history
…pment purposes.
  • Loading branch information
DiamondJoseph committed Jan 14, 2025
1 parent 97331ff commit cd69a67
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 23 deletions.
15 changes: 15 additions & 0 deletions example_configs/mock-oidc-server.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
authentication:
providers:
- provider: localhost
authenticator: tiled.authenticators:OIDCAuthenticator
args:
audience: tiled # something unique to ensure received headers are for you
# These values come from https://console.cloud.google.com/apis/credential
client_id: tiled
client_secret: secret
well_known_uri: http://localhost:8080/.well-known/openid-configuration
trees:
# Just some arbitrary example data...
# The point of this example is the authenticaiton above.
- tree: tiled.examples.generated_minimal:tree
path: /
28 changes: 28 additions & 0 deletions oidc/clients-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[
{
"ClientId": "tiled",
"ClientSecrets": ["secret"],
"Description": "Tiled web interface login",
"AllowedGrantTypes": ["implicit"],
"AllowedScopes": ["tiled", "openid", "profile", "email"],
"RedirectUris": ["localhost:4000/*"],
"AllowAccessTokensViaBrowser": true,
"AccessTokenLifetime": 3600,
"IdentityTokenLifetime": 3600
},
{
"ClientId": "blueapi",
"ClientSecrets": ["secret"],
"Description": "Blueapi CLI login",
"AllowedGrantTypes": ["urn:ietf:params:oauth:grant-type:device_code"],
"AllowedScopes": ["blueapi", "openid", "profile", "email", "offline_access"],
"AccessTokenLifetime": 3600,
"IdentityTokenLifetime": 3600,
"AllowOfflineAccess": true,
"Claims": [{
"Type": "aud",
"Value": "blueapi",
"ValueType": "string"
}]
}
]
69 changes: 69 additions & 0 deletions oidc/oidc-docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
services:
oidc-server-mock:
container_name: oidc-server-mock
image: ghcr.io/soluto/oidc-server-mock:latest
ports:
- 8080:80
environment:
ASPNETCORE_ENVIRONMENT: Development
ASPNETCORE_URLS: http://+:80
# ASPNETCORE_Kestrel__Certificates__Default__Password: <password for pfx file>
# ASPNETCORE_Kestrel__Certificates__Default__Path: /path/to/pfx/file
SERVER_OPTIONS_INLINE: |
{
"AccessTokenJwtType": "JWT",
"Discovery": {
"ShowKeySet": true
},
"Authentication": {
"CookieSameSiteMode": "Lax",
"CheckSessionCookieSameSiteMode": "Lax"
}
}
LOGIN_OPTIONS_INLINE: |
{
"AllowRememberLogin": false,
"AllowOfflineAccess": true
}
LOGOUT_OPTIONS_INLINE: |
{
"AutomaticRedirectAfterSignOut": true
}
API_SCOPES_INLINE: |
- Name: blueapi
- Name: tiled
API_RESOURCES_INLINE: |
- Name: app
Scopes:
- blueapi
- tiled
USERS_CONFIGURATION_INLINE: |
[
{
"SubjectId":"1",
"Username":"user",
"Password":"password",
"Claims": [
{
"Type": "name",
"Value": "Joe Bloggs",
"ValueType": "string"
},
{
"Type": "email",
"Value": "[email protected]",
"ValueType": "string"
}
]
}
]
CLIENTS_CONFIGURATION_PATH: /tmp/config/clients-config.json
ASPNET_SERVICES_OPTIONS_INLINE: |
{
"BasePath": "/foo",
"ForwardedHeadersOptions": {
"ForwardedHeaders" : "All"
}
}
volumes:
- .:/tmp/config:ro
10 changes: 5 additions & 5 deletions tiled/commandline/_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def serve_directory(
"example: --host `'::'`."
),
),
port: int = typer.Option(8000, help="Bind to a socket with this port."),
port: int = typer.Option(4000, help="Bind to a socket with this port."),
log_config: Optional[str] = typer.Option(
None, help="Custom uvicorn logging configuration file"
),
Expand Down Expand Up @@ -335,7 +335,7 @@ def serve_catalog(
"example: --host `'::'`."
),
),
port: int = typer.Option(8000, help="Bind to a socket with this port."),
port: int = typer.Option(4000, help="Bind to a socket with this port."),
scalable: bool = typer.Option(
False,
"--scalable",
Expand Down Expand Up @@ -499,7 +499,7 @@ def serve_pyobject(
"example: --host `'::'`."
),
),
port: int = typer.Option(8000, help="Bind to a socket with this port."),
port: int = typer.Option(4000, help="Bind to a socket with this port."),
scalable: bool = typer.Option(
False,
"--scalable",
Expand Down Expand Up @@ -547,7 +547,7 @@ def serve_demo(
"example: --host `'::'`."
),
),
port: int = typer.Option(8000, help="Bind to a socket with this port."),
port: int = typer.Option(4000, help="Bind to a socket with this port."),
):
"Start a public server with example data."
from ..server.app import build_app, print_admin_api_key_if_generated
Expand Down Expand Up @@ -650,7 +650,7 @@ def serve_config(
uvicorn_kwargs = parsed_config.pop("uvicorn", {})
# If --host is given, it overrides host in config. Same for --port and --log-config.
uvicorn_kwargs["host"] = host or uvicorn_kwargs.get("host", "127.0.0.1")
uvicorn_kwargs["port"] = port or uvicorn_kwargs.get("port", 8000)
uvicorn_kwargs["port"] = port or uvicorn_kwargs.get("port", 4000)
uvicorn_kwargs["log_config"] = _setup_log_config(
log_config or uvicorn_kwargs.get("log_config"),
log_timestamps,
Expand Down
2 changes: 1 addition & 1 deletion tiled/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
current_principal = contextvars.ContextVar("current_principal")


def custom_openapi(app):
def custom_openapi(app: FastAPI):
"""
The app's openapi method will be monkey-patched with this.
Expand Down
28 changes: 14 additions & 14 deletions tiled/server/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
from fastapi.security.api_key import APIKeyBase, APIKeyCookie, APIKeyQuery
from fastapi.security.utils import get_authorization_scheme_param
from fastapi.templating import Jinja2Templates
from pydantic_settings import BaseSettings
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
from sqlalchemy.sql import func
Expand Down Expand Up @@ -63,7 +62,7 @@
from . import schemas
from .core import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, json_or_msgpack
from .protocols import UsernamePasswordAuthenticator, UserSessionState
from .settings import get_settings
from .settings import Settings, get_settings
from .utils import API_KEY_COOKIE_NAME, get_authenticators, get_base_url

ALGORITHM = "HS256"
Expand Down Expand Up @@ -167,7 +166,7 @@ def create_refresh_token(session_id, secret_key, expires_delta):
return encoded_jwt


def decode_token(token, secret_keys):
def decode_token(token: str, secret_keys: list[str]):
credentials_exception = HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
Expand Down Expand Up @@ -220,8 +219,9 @@ async def get_decoded_access_token(
request: Request,
security_scopes: SecurityScopes,
access_token: str = Depends(oauth2_scheme),
settings: BaseSettings = Depends(get_settings),
settings: Settings = Depends(get_settings),
):
print("Got access_token")
if not access_token:
return None
try:
Expand All @@ -245,7 +245,7 @@ async def get_current_principal(
security_scopes: SecurityScopes,
decoded_access_token: str = Depends(get_decoded_access_token),
api_key: str = Depends(get_api_key),
settings: BaseSettings = Depends(get_settings),
settings: Settings = Depends(get_settings),
authenticators=Depends(get_authenticators),
db=Depends(get_database_session),
):
Expand Down Expand Up @@ -404,7 +404,7 @@ async def create_pending_session(db):


async def create_session(
settings, db, identity_provider, id, state: UserSessionState = None
settings: Settings, db, identity_provider, id, state: UserSessionState = None
):
# Have we seen this Identity before?
identity = (
Expand Down Expand Up @@ -463,7 +463,7 @@ async def create_session(
return fully_loaded_session


async def create_tokens_from_session(settings, db, session, provider):
async def create_tokens_from_session(settings: Settings, db, session, provider):
# Provide enough information in the access token to reconstruct Principal
# and its Identities sufficient for access policy enforcement without a
# database hit.
Expand Down Expand Up @@ -515,7 +515,7 @@ def build_auth_code_route(authenticator, provider):

async def route(
request: Request,
settings: BaseSettings = Depends(get_settings),
settings: Settings = Depends(get_settings),
db=Depends(get_database_session),
):
request.state.endpoint = "auth"
Expand Down Expand Up @@ -613,7 +613,7 @@ async def route(
code: str = Form(),
user_code: str = Form(),
state: Optional[str] = None,
settings: BaseSettings = Depends(get_settings),
settings: Settings = Depends(get_settings),
db=Depends(get_database_session),
):
request.state.endpoint = "auth"
Expand Down Expand Up @@ -676,7 +676,7 @@ def build_device_code_token_route(authenticator, provider):
async def route(
request: Request,
body: schemas.DeviceCode,
settings: BaseSettings = Depends(get_settings),
settings: Settings = Depends(get_settings),
db=Depends(get_database_session),
):
request.state.endpoint = "auth"
Expand Down Expand Up @@ -718,7 +718,7 @@ def build_handle_credentials_route(
async def route(
request: Request,
form_data: OAuth2PasswordRequestForm = Depends(),
settings: BaseSettings = Depends(get_settings),
settings: Settings = Depends(get_settings),
db=Depends(get_database_session),
):
request.state.endpoint = "auth"
Expand Down Expand Up @@ -970,7 +970,7 @@ async def apikey_for_principal(
async def refresh_session(
request: Request,
refresh_token: schemas.RefreshToken,
settings: BaseSettings = Depends(get_settings),
settings: Settings = Depends(get_settings),
db=Depends(get_database_session),
):
"Obtain a new access token and refresh token."
Expand All @@ -983,7 +983,7 @@ async def refresh_session(
async def revoke_session(
request: Request,
refresh_token: schemas.RefreshToken,
settings: BaseSettings = Depends(get_settings),
settings: Settings = Depends(get_settings),
db=Depends(get_database_session),
):
"Mark a Session as revoked so it cannot be refreshed again."
Expand Down Expand Up @@ -1025,7 +1025,7 @@ async def revoke_session_by_id(
return Response(status_code=HTTP_204_NO_CONTENT)


async def slide_session(refresh_token, settings, db):
async def slide_session(refresh_token, settings: Settings, db):
try:
payload = decode_token(refresh_token, settings.secret_keys)
except ExpiredSignatureError:
Expand Down
6 changes: 3 additions & 3 deletions tiled/server/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import secrets
from datetime import timedelta
from functools import lru_cache
from functools import cache
from typing import Any, List, Optional

from pydantic_settings import BaseSettings
Expand Down Expand Up @@ -79,6 +79,6 @@ def database_settings(self):
)


@lru_cache()
def get_settings():
@cache
def get_settings() -> Settings:
return Settings()

0 comments on commit cd69a67

Please sign in to comment.