Skip to content

Commit ce788f8

Browse files
committed
Add sample app that uses fastapi_sso
1 parent a3c8e1e commit ce788f8

File tree

8 files changed

+517
-0
lines changed

8 files changed

+517
-0
lines changed

examples/airnominal/.env

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
AIRNOMINAL_GITHUB_CLIENT_ID=eccd08d6736b7999a32a
2+
AIRNOMINAL_GITHUB_CLIENT_SECRET=642999c1c5f2b3df8b877afdc78252ef5b594d31
3+
AIRNOMINAL_GITHUB_REDIRECT_URL=http://127.0.0.1:8000/auth/callback
4+
AIRNMONIAL_MAIN_PAGE_REDIRECT_URL=http://127.0.0.1:8000/auth/status
5+
6+
AIRNOMINAL_JWT_SECRET_KEY=test
7+
AIRNOMINAL_JWT_ALGORITHM=HS256
8+
AIRNOMINAL_JWT_TOKEN_EXPIRES=300
9+
10+
AIRNOMINAL_INFLUX_BUCKET=
11+
AIRNOMINAL_INFLUX_ORG=
12+
AIRNOMINAL_INFLUX_TOKEN=
13+
AIRNOMINAL_INFLUX_URL=
14+
15+
AIRNOMINAL_MONGO_URL=mongodb://127.0.0.1:27017
16+
AIRNOMINAL_MONGO_PORT=27017
17+
AIRNOMINAL_MONGO_USERNAME=
18+
AIRNOMINAL_MONGO_PASSWORD=
19+
20+
AIRNOMINAL_ROOT_PATH=

examples/airnominal/auth.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import os
2+
from datetime import datetime, timedelta
3+
from typing import Optional
4+
5+
from fastapi import APIRouter
6+
from fastapi import Depends, HTTPException
7+
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
8+
from fastapi.responses import RedirectResponse
9+
from fastapi.security import HTTPBearer
10+
from fastapi.security import OAuth2
11+
from fastapi.security.base import SecurityBase
12+
from fastapi.security.utils import get_authorization_scheme_param
13+
from fastapi_sso.sso.github import GithubSSO
14+
from jose import jwt
15+
from pydantic import BaseModel
16+
from starlette.requests import Request
17+
from starlette.status import HTTP_403_FORBIDDEN
18+
19+
from config import CLIENT_ID, CLIENT_SECRET, redirect_url, SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES, \
20+
redirect_url_main_page
21+
22+
router = APIRouter()
23+
24+
# config for github SSO
25+
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
26+
27+
sso = GithubSSO(
28+
client_id=CLIENT_ID,
29+
client_secret=CLIENT_SECRET,
30+
redirect_uri=redirect_url,
31+
allow_insecure_http=True,
32+
)
33+
34+
security = HTTPBearer()
35+
36+
37+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
38+
to_encode = data.copy()
39+
if expires_delta:
40+
expire = datetime.utcnow() + expires_delta
41+
else:
42+
expire = datetime.utcnow() + timedelta(minutes=15)
43+
to_encode.update({"exp": expire})
44+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
45+
return encoded_jwt
46+
47+
48+
class Token(BaseModel):
49+
access_token: str
50+
token_type: str
51+
52+
53+
class TokenData(BaseModel):
54+
username: str = None
55+
56+
57+
class User(BaseModel):
58+
username: str
59+
email: str = None
60+
full_name: str = None
61+
disabled: bool = None
62+
63+
64+
class UserInDB(User):
65+
hashed_password: str
66+
67+
68+
class OAuth2PasswordBearerCookie(OAuth2):
69+
def __init__(
70+
self,
71+
tokenUrl: str,
72+
scheme_name: str = None,
73+
scopes: dict = None,
74+
auto_error: bool = True,
75+
):
76+
if not scopes:
77+
scopes = {}
78+
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
79+
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
80+
81+
async def __call__(self, request: Request) -> Optional[str]:
82+
header_authorization: str = request.headers.get("Authorization")
83+
cookie_authorization: str = request.cookies.get("Authorization")
84+
85+
header_scheme, header_param = get_authorization_scheme_param(
86+
header_authorization
87+
)
88+
cookie_scheme, cookie_param = get_authorization_scheme_param(
89+
cookie_authorization
90+
)
91+
92+
if header_scheme.lower() == "bearer":
93+
authorization = True
94+
scheme = header_scheme
95+
param = header_param
96+
97+
elif cookie_scheme.lower() == "bearer":
98+
authorization = True
99+
scheme = cookie_scheme
100+
param = cookie_param
101+
102+
else:
103+
authorization = False
104+
105+
if not authorization or scheme.lower() != "bearer":
106+
if self.auto_error:
107+
raise HTTPException(
108+
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
109+
)
110+
else:
111+
return None
112+
return param
113+
114+
115+
class BasicAuth(SecurityBase):
116+
def __init__(self, scheme_name: str = None, auto_error: bool = True):
117+
self.scheme_name = scheme_name or self.__class__.__name__
118+
self.auto_error = auto_error
119+
120+
async def __call__(self, request: Request) -> Optional[str]:
121+
authorization: str = request.headers.get("Authorization")
122+
scheme, param = get_authorization_scheme_param(authorization)
123+
if not authorization or scheme.lower() != "basic":
124+
if self.auto_error:
125+
raise HTTPException(
126+
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
127+
)
128+
else:
129+
return None
130+
return param
131+
132+
133+
basic_auth = BasicAuth(auto_error=False)
134+
135+
oauth2_scheme = OAuth2PasswordBearerCookie(tokenUrl="/token")
136+
137+
138+
async def get_current_user(token: str = Depends(oauth2_scheme)):
139+
credentials_exception = HTTPException(
140+
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
141+
)
142+
try:
143+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
144+
return payload
145+
except PyJWTError:
146+
raise credentials_exception
147+
148+
149+
@router.get("/auth/login")
150+
async def auth_init():
151+
"""Initialize auth and redirect"""
152+
return await sso.get_login_redirect()
153+
154+
155+
@router.get("/auth/callback")
156+
async def auth_callback(request: Request):
157+
"""Verify login"""
158+
user = await sso.verify_and_process(request)
159+
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
160+
access_token = create_access_token(
161+
data=dict(user), expires_delta=access_token_expires
162+
)
163+
print(dict(user))
164+
response = RedirectResponse(redirect_url_main_page)
165+
response.set_cookie(
166+
"Authorization",
167+
value=f"Bearer {access_token}",
168+
httponly=True,
169+
max_age=1800,
170+
expires=1800,
171+
)
172+
return response
173+
174+
175+
@router.get("/auth/logout")
176+
async def auth_logout():
177+
response = RedirectResponse(redirect_url_main_page)
178+
response.delete_cookie("Authorization")
179+
return response
180+
181+
182+
@router.get("/auth/status")
183+
async def auth_status(user=Depends(get_current_user)):
184+
return {
185+
"ok": True,
186+
"user": user,
187+
}

examples/airnominal/config.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import os
2+
3+
from dotenv import load_dotenv
4+
5+
load_dotenv()
6+
7+
# config for github SSO
8+
CLIENT_ID = os.getenv("AIRNOMINAL_GITHUB_CLIENT_ID")
9+
CLIENT_SECRET = os.getenv("AIRNOMINAL_GITHUB_CLIENT_SECRET")
10+
redirect_url = os.getenv("AIRNOMINAL_GITHUB_REDIRECT_URL")
11+
redirect_url_main_page = os.getenv("AIRNMONIAL_MAIN_PAGE_REDIRECT_URL")
12+
13+
# config for jwt generation
14+
SECRET_KEY = os.getenv("AIRNOMINAL_JWT_SECRET_KEY")
15+
ALGORITHM = os.getenv("AIRNOMINAL_JWT_ALGORITHM")
16+
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("AIRNOMINAL_JWT_TOKEN_EXPIRES"))
17+
18+
bucket = os.getenv("AIRNOMINAL_INFLUX_BUCKET")
19+
org = os.getenv("AIRNOMINAL_INFLUX_ORG")
20+
token = os.getenv("AIRNOMINAL_INFLUX_TOKEN")
21+
influx_url = os.getenv("AIRNOMINAL_INFLUX_URL")
22+
23+
mongo_url = os.getenv("AIRNOMINAL_MONGO_URL")
24+
port = int(os.getenv("AIRNOMINAL_MONGO_PORT") or "0")
25+
username = os.getenv("AIRNOMINAL_MONGO_USERNAME")
26+
password = os.getenv("AIRNOMINAL_MONGO_PASSWORD")
27+
28+
api_root_path = os.getenv("AIRNOMINAL_ROOT_PATH") or ""

examples/airnominal/data_endpoint.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import datetime
2+
3+
from fastapi import APIRouter
4+
5+
from mongo import stations, tokens
6+
7+
router = APIRouter()
8+
from pydantic import BaseModel
9+
from influx import writeJsonToInflux
10+
11+
"""
12+
f4174be6-5b5d-4094-8d38-644563608507_505524_0000000063FE3F23_DDF_3.1567_4DD_66567_LAT25.4_LON13.4END
13+
UUUUUUUU-UUUU-UUUU-UUUU-UUUUUUUUUUUU_PPPPPP_TTTTTTTTTTTTTTTT_LAT_NNNN_LON_NNNN_XXX_NNNNNN_XXX_NNNNNEND
14+
15+
U - station uuid
16+
P - password
17+
X - 3 leter sensor code
18+
N - sensor value
19+
f - end of number
20+
LAT - latitude
21+
LON - longitude
22+
END - stop of message, possible start of new message
23+
T - unix time stamp in base 16
24+
25+
"""
26+
27+
28+
def dataParser(raw: str):
29+
parsed = []
30+
print(raw)
31+
for dt in [x for x in raw.split("END") if len(x) > 0]:
32+
data = dt.split("_")
33+
print(data)
34+
uu = data[0]
35+
pas = data[1]
36+
if "T" in data[2]:
37+
time = datetime.datetime.utcnow().isoformat()
38+
else:
39+
time = datetime.datetime.utcfromtimestamp(int(data[2], base=16)).isoformat()
40+
lat = float(data[4])
41+
lon = float(data[6])
42+
sensor_data = []
43+
for i in range(7, len(data), 2):
44+
sen = data[i]
45+
dat = float(data[i + 1])
46+
sensor_data.append({
47+
"short_id": sen,
48+
"data": dat
49+
})
50+
parsed.append({
51+
"station_id": uu,
52+
"token": pas,
53+
"time": time,
54+
"lat": lat,
55+
"lon": lon,
56+
"data": sensor_data
57+
})
58+
print(parsed)
59+
return parsed
60+
61+
62+
"""
63+
p = influxdb_client.Point("data").from_dict(
64+
65+
{
66+
"measurement": "data",
67+
"tags": {
68+
"owner_id": "1235989238",
69+
"owner_name": "Alenka Mozer",
70+
"station_id": "3894838983",
71+
"station_name": "test station",
72+
"sensor_id": "878754375",
73+
"sensor_name": "CO2-01",
74+
"display_quantity": "CO2 (ppm)",
75+
"quantity": "CO2",
76+
"unit": "ppm"
77+
},
78+
"time": random_date_time(),
79+
"fields": {
80+
"lat": random.uniform(-180, 180),
81+
"lon": random.uniform(-90, 90),
82+
"value": random.uniform(0, 30)
83+
}
84+
}
85+
86+
)
87+
"""
88+
89+
90+
def influxDataTransformer(parsed_object):
91+
query = {"station_id": parsed_object["station_id"]}
92+
station = stations.find_one(query)
93+
token = tokens.find_one(query)
94+
if parsed_object["token"] != token["token"]:
95+
return False
96+
for point in parsed_object["data"]:
97+
sensor = [sen for sen in station["sensors"] if sen["short_id"] == point["short_id"]][0]
98+
writeJsonToInflux({
99+
"measurement": "data",
100+
"tags": {
101+
"owner_id": station["owner_id"],
102+
"owner_name": station["owner_name"],
103+
"station_id": parsed_object["station_id"],
104+
"station_name": station["station_name"],
105+
"sensor_id": sensor["sensor_id"],
106+
"sensor_name": sensor["sensor_name"],
107+
"display_quantity": sensor["quantity"] + " (" + sensor["unit"] + ")",
108+
"quantity": sensor["quantity"],
109+
"unit": sensor["unit"]
110+
},
111+
"time": parsed_object["time"],
112+
"fields": {
113+
"lat": parsed_object["lat"],
114+
"lon": parsed_object["lon"],
115+
"value": point["data"]
116+
}
117+
})
118+
time = datetime.datetime.fromisoformat(parsed_object["time"] + "+00:00")
119+
update = {"$set": {"lat": parsed_object["lat"], "lon": parsed_object["lon"], "updated": time}}
120+
stations.update_one(query, update)
121+
122+
123+
class DataPost(BaseModel):
124+
data: str
125+
126+
127+
@router.post("/data")
128+
async def ingest_data(item: DataPost):
129+
for point in dataParser(item.data):
130+
influxDataTransformer(point)
131+
return True
132+
133+
134+
@router.post("/data_url")
135+
async def ingest_data(item: str = ""):
136+
if item != "":
137+
for point in dataParser(item):
138+
influxDataTransformer(point)
139+
return True

examples/airnominal/influx.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import influxdb_client
2+
from influxdb_client.client.write_api import SYNCHRONOUS
3+
4+
from config import bucket, org, token
5+
from config import influx_url as url
6+
7+
client = influxdb_client.InfluxDBClient(
8+
url=url,
9+
token=token,
10+
org=org
11+
)
12+
13+
# Write script
14+
write_api = client.write_api(write_options=SYNCHRONOUS)
15+
16+
17+
def writeJsonToInflux(dict):
18+
print(dict)
19+
p = influxdb_client.Point("data").from_dict(dict)
20+
write_api.write(bucket=bucket, org=org, record=p)

0 commit comments

Comments
 (0)