Commit
·
8393a26
0
Parent(s):
Added SSO Backend Functionality
Browse files- .gitignore +41 -0
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/__pycache__/main.cpython-312.pyc +0 -0
- app/api/__init__.py +0 -0
- app/api/__pycache__/__init__.cpython-312.pyc +0 -0
- app/api/v1/__init__.py +0 -0
- app/api/v1/__pycache__/__init__.cpython-312.pyc +0 -0
- app/api/v1/__pycache__/api.cpython-312.pyc +0 -0
- app/api/v1/api.py +6 -0
- app/api/v1/endpoints/__init__.py +0 -0
- app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc +0 -0
- app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc +0 -0
- app/api/v1/endpoints/auth.py +64 -0
- app/core/__init__.py +0 -0
- app/core/__pycache__/__init__.cpython-312.pyc +0 -0
- app/core/__pycache__/config.cpython-312.pyc +0 -0
- app/core/config.py +17 -0
- app/main.py +33 -0
- app/templates/index.html +32 -0
- requirements.txt +8 -0
.gitignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
|
| 36 |
+
# vercel
|
| 37 |
+
.vercel
|
| 38 |
+
|
| 39 |
+
# typescript
|
| 40 |
+
*.tsbuildinfo
|
| 41 |
+
next-env.d.ts
|
app/__init__.py
ADDED
|
File without changes
|
app/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (175 Bytes). View file
|
|
|
app/__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (1.55 kB). View file
|
|
|
app/api/__init__.py
ADDED
|
File without changes
|
app/api/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (179 Bytes). View file
|
|
|
app/api/v1/__init__.py
ADDED
|
File without changes
|
app/api/v1/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (182 Bytes). View file
|
|
|
app/api/v1/__pycache__/api.cpython-312.pyc
ADDED
|
Binary file (458 Bytes). View file
|
|
|
app/api/v1/api.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from app.api.v1.endpoints import auth
|
| 3 |
+
|
| 4 |
+
api_router = APIRouter()
|
| 5 |
+
|
| 6 |
+
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
app/api/v1/endpoints/__init__.py
ADDED
|
File without changes
|
app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (192 Bytes). View file
|
|
|
app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc
ADDED
|
Binary file (2.93 kB). View file
|
|
|
app/api/v1/endpoints/auth.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Request, HTTPException
|
| 2 |
+
from authlib.integrations.starlette_client import OAuth
|
| 3 |
+
from app.core.config import settings
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
# Initialize OAuth
|
| 8 |
+
oauth = OAuth()
|
| 9 |
+
|
| 10 |
+
# Register Microsoft Provider
|
| 11 |
+
# We use the tenant-specific endpoint for better security and internal organization
|
| 12 |
+
CONF_URL = f"https://login.microsoftonline.com/{settings.MS_TENANT_ID}/v2.0/.well-known/openid-configuration"
|
| 13 |
+
|
| 14 |
+
oauth.register(
|
| 15 |
+
name='microsoft',
|
| 16 |
+
client_id=settings.MS_CLIENT_ID,
|
| 17 |
+
client_secret=settings.MS_CLIENT_SECRET,
|
| 18 |
+
server_metadata_url=CONF_URL,
|
| 19 |
+
client_kwargs={
|
| 20 |
+
'scope': 'openid email profile User.Read'
|
| 21 |
+
}
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
@router.get("/login")
|
| 25 |
+
async def login(request: Request):
|
| 26 |
+
"""
|
| 27 |
+
Redirects the user to the Microsoft Login page.
|
| 28 |
+
"""
|
| 29 |
+
redirect_uri = settings.MS_REDIRECT_URI
|
| 30 |
+
return await oauth.microsoft.authorize_redirect(request, redirect_uri)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@router.get("/callback")
|
| 34 |
+
async def auth_callback(request: Request):
|
| 35 |
+
"""
|
| 36 |
+
Handles the callback from Microsoft after successful login.
|
| 37 |
+
Exchanges the authorization code for an access token and user info.
|
| 38 |
+
"""
|
| 39 |
+
try:
|
| 40 |
+
token = await oauth.microsoft.authorize_access_token(request)
|
| 41 |
+
user = token.get('userinfo')
|
| 42 |
+
|
| 43 |
+
if not user:
|
| 44 |
+
# Sometimes userinfo is not directly in the token depending on claims,
|
| 45 |
+
# allow fetch via userinfo endpoint if configured, or parse id_token
|
| 46 |
+
user = await oauth.microsoft.userinfo(token=token)
|
| 47 |
+
|
| 48 |
+
# Store user info in session (cookie)
|
| 49 |
+
# In a real app, you might issue your own JWT here instead
|
| 50 |
+
request.session['user'] = dict(user)
|
| 51 |
+
|
| 52 |
+
return {"message": "Login successful", "user": user}
|
| 53 |
+
except Exception as e:
|
| 54 |
+
# Log the error in production
|
| 55 |
+
raise HTTPException(status_code=400, detail=f"SSO Login Failed: {str(e)}")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@router.get("/logout")
|
| 59 |
+
async def logout(request: Request):
|
| 60 |
+
"""
|
| 61 |
+
Clears the local session.
|
| 62 |
+
"""
|
| 63 |
+
request.session.pop('user', None)
|
| 64 |
+
return {"message": "Logged out successfully"}
|
app/core/__init__.py
ADDED
|
File without changes
|
app/core/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (180 Bytes). View file
|
|
|
app/core/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (888 Bytes). View file
|
|
|
app/core/config.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings
|
| 2 |
+
|
| 3 |
+
class Settings(BaseSettings):
|
| 4 |
+
PROJECT_NAME: str = "FastAPI SSO"
|
| 5 |
+
SECRET_KEY: str
|
| 6 |
+
|
| 7 |
+
# Microsoft SSO Config
|
| 8 |
+
MS_CLIENT_ID: str
|
| 9 |
+
MS_CLIENT_SECRET: str
|
| 10 |
+
MS_TENANT_ID: str
|
| 11 |
+
MS_REDIRECT_URI: str
|
| 12 |
+
|
| 13 |
+
class Config:
|
| 14 |
+
env_file = ".env"
|
| 15 |
+
case_sensitive = True
|
| 16 |
+
|
| 17 |
+
settings = Settings()
|
app/main.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from starlette.middleware.sessions import SessionMiddleware
|
| 3 |
+
from starlette.requests import Request
|
| 4 |
+
from starlette.responses import HTMLResponse
|
| 5 |
+
from starlette.templating import Jinja2Templates
|
| 6 |
+
|
| 7 |
+
from app.core.config import settings
|
| 8 |
+
from app.api.v1.api import api_router
|
| 9 |
+
|
| 10 |
+
app = FastAPI(title=settings.PROJECT_NAME)
|
| 11 |
+
|
| 12 |
+
# 1. Add Session Middleware
|
| 13 |
+
# This is required for Authlib to store the temporary state during the OAuth dance
|
| 14 |
+
app.add_middleware(
|
| 15 |
+
SessionMiddleware,
|
| 16 |
+
secret_key=settings.SECRET_KEY,
|
| 17 |
+
https_only=False # Set to True in production with SSL
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# 2. Include API Routers
|
| 21 |
+
app.include_router(api_router)
|
| 22 |
+
|
| 23 |
+
# 3. Simple Frontend for demonstration
|
| 24 |
+
templates = Jinja2Templates(directory="app/templates")
|
| 25 |
+
|
| 26 |
+
@app.get("/", response_class=HTMLResponse)
|
| 27 |
+
async def root(request: Request):
|
| 28 |
+
user = request.session.get("user")
|
| 29 |
+
return templates.TemplateResponse("index.html", {"request": request, "user": user})
|
| 30 |
+
|
| 31 |
+
if __name__ == "__main__":
|
| 32 |
+
import uvicorn
|
| 33 |
+
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
app/templates/index.html
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>FastAPI Microsoft SSO</title>
|
| 7 |
+
<style>
|
| 8 |
+
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f4f4f4; }
|
| 9 |
+
.container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); text-align: center; }
|
| 10 |
+
.btn { display: inline-block; padding: 10px 20px; background-color: #0078d4; color: white; text-decoration: none; border-radius: 4px; font-weight: bold; }
|
| 11 |
+
.btn-logout { background-color: #d83b01; }
|
| 12 |
+
pre { background: #eee; padding: 10px; text-align: left; overflow-x: auto; }
|
| 13 |
+
</style>
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<div class="container">
|
| 17 |
+
<h1>Microsoft SSO Integration</h1>
|
| 18 |
+
|
| 19 |
+
{% if user %}
|
| 20 |
+
<p>Welcome, <strong>{{ user.name or user.email }}</strong>!</p>
|
| 21 |
+
<div style="text-align: left; margin-bottom: 20px;">
|
| 22 |
+
<small>User Details:</small>
|
| 23 |
+
<pre>{{ user | tojson(indent=2) }}</pre>
|
| 24 |
+
</div>
|
| 25 |
+
<a href="/auth/logout" class="btn btn-logout">Logout</a>
|
| 26 |
+
{% else %}
|
| 27 |
+
<p>You are not logged in.</p>
|
| 28 |
+
<a href="/auth/login" class="btn">Login with Microsoft</a>
|
| 29 |
+
{% endif %}
|
| 30 |
+
</div>
|
| 31 |
+
</body>
|
| 32 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
authlib
|
| 4 |
+
httpx
|
| 5 |
+
pydantic-settings
|
| 6 |
+
jinja2
|
| 7 |
+
python-multipart
|
| 8 |
+
itsdangerous
|