-
Notifications
You must be signed in to change notification settings - Fork 29
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
Port register, login and update services to the new ECS style #923
Changes from all commits
54ce170
f079cea
38f3b8a
481f9af
61ae79e
769a0e7
d1a8137
f3785be
c52c208
be3fb93
6cd9426
c69b37d
ddc93da
eb4cf8d
dc48d33
c5a05ea
b493e1f
884cb70
a76a17c
21a166c
dcf48d5
cc0342d
c164783
3679d4e
b7bb39f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import logging | ||
from datetime import datetime, timezone, timedelta | ||
import time | ||
from typing import List | ||
|
||
from fastapi import APIRouter, Depends, HTTPException, Response | ||
|
||
from ...common.dependencies import get_settings | ||
from ...common.routers import BaseModel | ||
from ...common.auth import create_jwt, decode_jwt, jwt | ||
from ...common.config import Settings | ||
from ...common.utils import setnocacheresponse | ||
|
||
router = APIRouter(prefix="/v1") | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class ProbeLogin(BaseModel): | ||
# Allow None username and password | ||
# to deliver informational 401 error when they're missing | ||
username: str | None = None | ||
# not actually used but necessary to be compliant with the old API schema | ||
password: str | None = None | ||
|
||
|
||
class ProbeLoginResponse(BaseModel): | ||
token: str | ||
expire: str | ||
|
||
|
||
@router.post("/login", tags=["ooniprobe"], response_model=ProbeLoginResponse) | ||
def probe_login_post( | ||
probe_login: ProbeLogin, | ||
response: Response, | ||
settings: Settings = Depends(get_settings), | ||
) -> ProbeLoginResponse: | ||
|
||
if probe_login.username is None or probe_login.password is None: | ||
raise HTTPException(status_code=401, detail="Missing credentials") | ||
|
||
token = probe_login.username | ||
# TODO: We have to find a way to explicitly log metrics with prometheus. | ||
# We're currently using the instrumentator default metrics, like http response counts | ||
# Maybe using the same exporter as the instrumentator? | ||
try: | ||
dec = decode_jwt(token, audience="probe_login", key=settings.jwt_encryption_key) | ||
registration_time = dec["iat"] | ||
log.info("probe login: successful") | ||
# metrics.incr("probe_login_successful") | ||
except jwt.exceptions.MissingRequiredClaimError: | ||
log.info("probe login: invalid or missing claim") | ||
# metrics.incr("probe_login_failed") | ||
raise HTTPException(status_code=401, detail="Invalid credentials") | ||
except jwt.exceptions.InvalidSignatureError: | ||
log.info("probe login: invalid signature") | ||
# metrics.incr("probe_login_failed") | ||
raise HTTPException(status_code=401, detail="Invalid credentials") | ||
except jwt.exceptions.DecodeError: | ||
# Not a JWT token: treat it as a "legacy" login | ||
# return jerror("Invalid or missing credentials", code=401) | ||
log.info("probe login: legacy login successful") | ||
# metrics.incr("probe_legacy_login_successful") | ||
registration_time = None | ||
|
||
exp = datetime.now(timezone.utc) + timedelta(days=7) | ||
payload = {"registration_time": registration_time, "aud": "probe_token"} | ||
token = create_jwt(payload, key=settings.jwt_encryption_key) | ||
# expiration string used by the probe e.g. 2006-01-02T15:04:05Z | ||
expire = exp.strftime("%Y-%m-%dT%H:%M:%SZ") | ||
login_response = ProbeLoginResponse(token=token, expire=expire) | ||
setnocacheresponse(response) | ||
|
||
return login_response | ||
|
||
|
||
class ProbeRegister(BaseModel): | ||
# None of this values is actually used, but I add them | ||
# to keep it compliant with the old api | ||
password: str | ||
platform: str | ||
probe_asn: str | ||
probe_cc: str | ||
software_name: str | ||
software_version: str | ||
supported_tests: List[str] | ||
|
||
|
||
class ProbeRegisterResponse(BaseModel): | ||
client_id: str | ||
|
||
|
||
@router.post("/register", tags=["ooniprobe"], response_model=ProbeRegisterResponse) | ||
def probe_register_post( | ||
probe_register: ProbeRegister, | ||
response: Response, | ||
settings: Settings = Depends(get_settings), | ||
) -> ProbeRegisterResponse: | ||
"""Probe Services: Register | ||
|
||
Probes send a random string called password and receive a client_id | ||
|
||
The client_id/password tuple is saved by the probe and long-lived | ||
|
||
Note that most of the request body arguments are not actually | ||
used but are kept here to use the same API as the old version | ||
|
||
""" | ||
|
||
# **IMPORTANT** You have to compute this token using a different key | ||
# to the one used in ooniauth service, because you could allow | ||
# a login bypass attack if you don't. | ||
# | ||
# Note that this token is generated regardless of any authentication, | ||
# so if you use the same jwt_encryption_key for ooniauth, you give users | ||
# an auth token for free | ||
# | ||
# We set this up in the terraform level | ||
|
||
# client_id is a JWT token with "issued at" claim and | ||
# "audience" claim. The "issued at" claim is rounded up. | ||
issued_at = int(time.time()) | ||
payload = {"iat": issued_at, "aud": "probe_login"} | ||
client_id = create_jwt(payload, key=settings.jwt_encryption_key) | ||
log.info("register successful") | ||
|
||
register_response = ProbeRegisterResponse(client_id=client_id) | ||
setnocacheresponse(response) | ||
|
||
return register_response | ||
|
||
|
||
class ProbeUpdate(BaseModel): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The ProbeUpdate request contains some useful metadata that we could use for the purpose of the anonymous credential anomaly detection to establish a baseline for comparing the probe_cc and probe_asn inconsistencies (see: https://github.com/ooni/orchestra/blob/master/registry/registry/handler/registry.go#L25). Basically what I'm thinking of is that, if we assume that the update requests are not going to be tampered with as much as the measurement submit ones, we can use this to determine which inconsistencies are due to our geoip being off compared to the probe one, vs those which are due to the measurement metadata being inconsistent due to malice. We can do parsing and logging of this as a follow up PR though. |
||
pass | ||
|
||
|
||
class ProbeUpdateResponse(BaseModel): | ||
status: str | ||
|
||
|
||
@router.put("/update/{client_id}", tags=["ooniprobe"]) | ||
def probe_update_post(probe_update: ProbeUpdate) -> ProbeUpdateResponse: | ||
log.info("update successful") | ||
return ProbeUpdateResponse(status="ok") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should take note of the fact the jwt_encryption_key is being shared for between probes and user authenticating via email in the auth service.
I don't think there is necessary a problem, given that the
aud
field is different, but we should make sure we don't forget to always checkaud
filed to avoid authentication bypasses.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following a discussion with @LDiazN we decided to actually change this to a dedicated JWT token which we generate and inject via terraform: https://github.com/ooni/devops/blob/main/tf/environments/dev/main.tf#L187