···7788from millipds import static_config
991010-with apsw.Connection(static_config.MAIN_DB_PATH) as con:
1010+1111+def migrate(con):
1112 version_now, *_ = con.execute("SELECT db_version FROM config").fetchone()
12131314 assert version_now == 1
···36373738 con.execute("UPDATE config SET db_version=2")
38393939-print("v1 -> v2 Migration successful")
4040+4141+if __name__ == "__main__":
4242+ with apsw.Connection(static_config.MAIN_DB_PATH) as con:
4343+ migrate(con)
4444+4545+ print("v1 -> v2 Migration successful")
+34
migration_scripts/v3.py
···11+# TODO: some smarter way of handling migrations
22+33+import apsw
44+import apsw.bestpractice
55+66+apsw.bestpractice.apply(apsw.bestpractice.recommended)
77+88+from millipds import static_config
99+1010+1111+def migrate(con: apsw.Connection):
1212+ version_now, *_ = con.execute("SELECT db_version FROM config").fetchone()
1313+1414+ assert version_now == 2
1515+1616+ con.execute(
1717+ """
1818+ CREATE TABLE revoked_token(
1919+ did TEXT NOT NULL,
2020+ jti TEXT NOT NULL,
2121+ expires_at INTEGER NOT NULL,
2222+ PRIMARY KEY (did, jti)
2323+ ) STRICT, WITHOUT ROWID
2424+ """
2525+ )
2626+2727+ con.execute("UPDATE config SET db_version=3")
2828+2929+3030+if __name__ == "__main__":
3131+ with apsw.Connection(static_config.MAIN_DB_PATH) as con:
3232+ migrate(con)
3333+3434+ print("v2 -> v3 Migration successful")
+7-5
src/millipds/appview_proxy.py
···2929 )
3030 if did_doc is None:
3131 return web.HTTPInternalServerError(
3232- f"unable to resolve service {service!r}"
3232+ text=f"unable to resolve service {service!r}"
3333 )
3434- for service in did_doc.get("service", []):
3535- if service.get("id") == fragment:
3636- service_route = service["serviceEndpoint"]
3434+ for service_info in did_doc.get("service", []):
3535+ if service_info.get("id") == fragment:
3636+ service_route = service_info["serviceEndpoint"]
3737 break
3838 else:
3939- return web.HTTPBadRequest(f"unable to resolve service {service!r}")
3939+ return web.HTTPBadRequest(
4040+ text=f"unable to resolve service {service!r}"
4141+ )
4042 else: # fall thru to assuming bsky appview
4143 service_did = db.config["bsky_appview_did"]
4244 service_route = db.config["bsky_appview_pfx"]
+50-25
src/millipds/auth_bearer.py
···1111routes = web.RouteTableDef()
121213131414+def verify_symmetric_token(
1515+ request: web.Request, token: str, expected_scope: str
1616+) -> dict:
1717+ db = get_db(request)
1818+ try:
1919+ payload: dict = jwt.decode(
2020+ jwt=token,
2121+ key=db.config["jwt_access_secret"],
2222+ algorithms=["HS256"],
2323+ audience=db.config["pds_did"],
2424+ options={
2525+ "require": ["exp", "iat", "scope", "jti", "sub"],
2626+ "verify_exp": True,
2727+ "verify_iat": True,
2828+ "strict_aud": True, # may be unnecessary
2929+ },
3030+ )
3131+ except jwt.exceptions.PyJWTError:
3232+ raise web.HTTPUnauthorized(text="invalid jwt")
3333+3434+ revoked = db.con.execute(
3535+ "SELECT COUNT(*) FROM revoked_token WHERE did=? AND jti=?",
3636+ (payload["sub"], payload["jti"]),
3737+ ).fetchone()[0]
3838+3939+ if revoked:
4040+ raise web.HTTPUnauthorized(text="revoked token")
4141+4242+ # if we reached this far, the payload must've been signed by us
4343+ if payload.get("scope") != expected_scope:
4444+ raise web.HTTPUnauthorized(text="invalid jwt scope")
4545+4646+ if not payload.get("sub", "").startswith("did:"):
4747+ raise web.HTTPUnauthorized(text="invalid jwt: invalid subject")
4848+4949+ return payload
5050+5151+1452def authenticated(handler):
1553 """
1654 There are three types of auth:
···3977 )
4078 # logger.info(unverified)
4179 if unverified["header"]["alg"] == "HS256": # symmetric secret
4242- try:
4343- payload: dict = jwt.decode(
4444- jwt=token,
4545- key=db.config["jwt_access_secret"],
4646- algorithms=["HS256"],
4747- audience=db.config["pds_did"],
4848- options={
4949- "require": ["exp", "iat", "scope"], # consider iat?
5050- "verify_exp": True,
5151- "verify_iat": True,
5252- "strict_aud": True, # may be unnecessary
5353- },
5454- )
5555- except jwt.exceptions.PyJWTError:
5656- raise web.HTTPUnauthorized(text="invalid jwt")
5757-5858- # if we reached this far, the payload must've been signed by us
5959- if payload.get("scope") != "com.atproto.access":
6060- raise web.HTTPUnauthorized(text="invalid jwt scope")
6161-6262- subject: str = payload.get("sub", "")
6363- if not subject.startswith("did:"):
6464- raise web.HTTPUnauthorized(text="invalid jwt: invalid subject")
6565- request["authed_did"] = subject
8080+ request["authed_did"] = verify_symmetric_token(
8181+ request, token, "com.atproto.access"
8282+ )["sub"]
6683 else: # asymmetric service auth (scoped to a specific lxm)
6784 did: str = unverified["payload"]["iss"]
6885 if not did.startswith("did:"):
···8198 algorithms=[alg],
8299 audience=db.config["pds_did"],
83100 options={
8484- "require": ["exp", "iat", "lxm"],
101101+ "require": ["exp", "iat", "lxm", "jti", "iss"],
85102 "verify_exp": True,
86103 "verify_iat": True,
87104 "strict_aud": True, # may be unnecessary
···89106 )
90107 except jwt.exceptions.PyJWTError:
91108 raise web.HTTPUnauthorized(text="invalid jwt")
109109+110110+ revoked = db.con.execute(
111111+ "SELECT COUNT(*) FROM revoked_token WHERE did=? AND jti=?",
112112+ (payload["iss"], payload["jti"]),
113113+ ).fetchone()[0]
114114+115115+ if revoked:
116116+ raise web.HTTPUnauthorized(text="revoked token")
9211793118 request_lxm = request.path.rpartition("/")[2].partition("?")[0]
94119 if request_lxm != payload.get("lxm"):
+13
src/millipds/database.py
···245245 """
246246 )
247247248248+ # this is only for the tokens *we* issue, dpop jti will be tracked separately
249249+ # there's no point remembering that an expired token was revoked, and we'll garbage-collect these periodically
250250+ self.con.execute(
251251+ """
252252+ CREATE TABLE revoked_token(
253253+ did TEXT NOT NULL,
254254+ jti TEXT NOT NULL,
255255+ expires_at INTEGER NOT NULL,
256256+ PRIMARY KEY (did, jti)
257257+ ) STRICT, WITHOUT ROWID
258258+ """
259259+ )
260260+248261 def update_config(
249262 self,
250263 pds_pfx: Optional[str] = None,