async def generate_refresh_token(email: str, client_app: ClientApp) -> str: if not client_app.get_refresh_key( ) or not client_app.refresh_token_expire_hours: raise TokenCreationError("Refresh is not enabled") uid = str(uuid.uuid4()) payload = { "iss": f"{config.ISSUER}/app/{client_app.app_id}", "sub": email, "uid": uid, } token = jwt.generate_jwt( payload, client_app.get_refresh_key(), "ES256", datetime.timedelta(hours=client_app.refresh_token_expire_hours), ) token_hash = PWD_CONTEXT.hash(token) expires = datetime.datetime.now() + datetime.timedelta( hours=client_app.refresh_token_expire_hours) await RefreshToken( app_id=client_app.app_id, email=email, hash=token_hash, expires=expires, uid=uid, ).insert() return token
async def user1_app_no_refresh(user1, faker, monkeypatch): app = ClientApp( name=faker.company(), app_id=str(uuid.uuid4()), refresh_token_expire_hours=None, redirect_url="https://example.com/magic", failure_redirect_url="https://example.com/failed", owner=user1.email, quota=500, low_quota_threshold=10, unlimited=False, ) key = jwk.JWK.generate(kty="EC", size=2048) app.set_key(key) await app.insert() return app
def generate(email: str, client_app: ClientApp) -> str: payload = {"iss": f"{config.ISSUER}/app/{client_app.app_id}", "sub": email} return jwt.generate_jwt( payload, client_app.get_key(), "ES256", datetime.timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES), )
async def create_client_app( app_name: str, owner: str, redirect_url: str, failure_redirect_url: str, refresh: bool = False, app_id: Optional[str] = None, refresh_token_expire_hours: int = 24, low_quota_threshold: int = 10, ) -> ClientApp: key = jwk.JWK.generate(kty="EC", size=2048) app_id = app_id or str(uuid.uuid4()) app = ClientApp( name=app_name, app_id=app_id, owner=owner, key=None, refresh_key=None, refresh_token_expire_hours=None, redirect_url=redirect_url, low_quota_threshold=low_quota_threshold, failure_redirect_url=failure_redirect_url, ) if refresh: app.set_refresh_key(jwk.JWK.generate(kty="EC", size=4096)) app.refresh_token_expire_hours = refresh_token_expire_hours app.set_key(key) await app.insert() return app
async def verify_refresh_token(token: str, client_app: ClientApp) -> str: _, claims = _check_token(token, client_app.get_refresh_key(), client_app.app_id) found_rt = await _find_refresh_token(claims, client_app) if found_rt.expires <= datetime.datetime.now(): await found_rt.delete() raise TokenVerificationError("Expired Token. Please log in again.") if PWD_CONTEXT.verify(token, found_rt.hash): return generate(claims["sub"], client_app) raise TokenVerificationError("Could not find matching refresh token")
async def confirm_otp( confirm_code: ConfirmCode, client_app: ClientApp = Depends(check_client_app), ): """Confirm authentication by one time code""" if not security_otp.verify(confirm_code.email, confirm_code.code, client_app.app_id): raise HTTPException(status_code=401, detail="Invalid Code.") id_token = security_token.generate(confirm_code.email, client_app) refresh_token = None if client_app.get_refresh_key(): refresh_token = await security_token.generate_refresh_token( confirm_code.email, client_app) return IssueToken(idToken=id_token, refreshToken=refresh_token)
async def confirm_magic( secret: str = Query(...), id_: str = Query(..., alias="id"), client_app: ClientApp = Depends(check_client_app), ): """This endpoint confirms magic links. Do not use directly.""" if email := security_magic.verify(id_, secret, client_app.app_id): id_token = security_token.generate(email, client_app) redirect_url = f"{client_app.redirect_url}?idToken={quote_plus(id_token)}" if client_app.get_refresh_key(): refresh_token = await security_token.generate_refresh_token( email, client_app ) redirect_url = f"{redirect_url}&refreshToken={quote_plus(refresh_token)}" return RedirectResponse(redirect_url)
def _create( app_id=None, refresh=False, refresh_expire=None, failure_redirect_url=None, owner=None, quota=500, low_quota_threshold=10, low_quota_last_notified=None, unlimited=False, app_name=None, ): if not app_id: app_id = str(uuid.uuid4()) if not app_name: app_name = faker.company() key = jwk.JWK.generate(kty="EC", size=2048) mocker.patch("mongox.Model.delete") mocker.patch("mongox.Model.save") _app = ClientApp( name=app_name, app_id=app_id, refresh_key=None, refresh_token_expire_hours=None, key=None, redirect_url="http://localhost", failure_redirect_url=failure_redirect_url, owner=owner, quota=quota, low_quota_threshold=low_quota_threshold, unlimited=unlimited, ) if low_quota_last_notified: _app.low_quota_last_notified = low_quota_last_notified _app.set_key(key) if refresh: _app.set_refresh_key(jwk.JWK.generate(kty="EC", size=4096)) _app.refresh_token_expire_hours = refresh_expire or 24 return _app
async def test_generate_refresh_token( fake_email, fake_refresh_client_app: ClientApp, monkeypatch, pwd_context, ): fake_uuid = uuid.uuid4() monkeypatch.setattr(uuid, "uuid4", lambda: fake_uuid) refresh_token = await security_token.generate_refresh_token( fake_email, fake_refresh_client_app) assert refresh_token is not None headers, claims = jwt.verify_jwt(refresh_token, fake_refresh_client_app.get_refresh_key(), allowed_algs=["ES256"]) assert headers["alg"] == "ES256" assert claims["sub"] == fake_email assert claims[ "iss"] == f"{config.ISSUER}/app/{fake_refresh_client_app.app_id}" assert claims["uid"] == str(fake_uuid) # mock_insert.assert_called() # getting the seconds just right is annoying, so it's easiest just to check that # it's within about 10 minutes of the correct time. should_expire_lower_bound = (datetime.datetime.now() + datetime.timedelta( hours=fake_refresh_client_app.refresh_token_expire_hours) - datetime.timedelta(minutes=5)) should_expire_upper_bound = (datetime.datetime.now() + datetime.timedelta( hours=fake_refresh_client_app.refresh_token_expire_hours) + datetime.timedelta(minutes=5)) # print(dir(mock_insert)) generated_rt: RefreshToken = (await RefreshToken.query( RefreshToken.email == claims["sub"] ).query(RefreshToken.app_id == fake_refresh_client_app.app_id ).query(RefreshToken.uid == claims["uid"]).get()) assert generated_rt.expires >= should_expire_lower_bound assert generated_rt.expires <= should_expire_upper_bound assert pwd_context.verify(refresh_token, generated_rt.hash)
def createapp( app_name: str, url: str, refresh: bool, refresh_token_expire_hours: Optional[int], app_id: Optional[str], ): key = jwk.JWK.generate(kty="EC", size=2048) app_id = app_id or str(uuid.uuid4()) app = ClientApp( name=app_name, app_id=app_id, key=None, refresh_key=None, refresh_token_expire_hours=None, redirect_url=url, owner=config.WEBMASTER_EMAIL, ) if refresh: app.set_refresh_key(jwk.JWK.generate(kty="EC", size=4096)) app.refresh_token_expire_hours = refresh_token_expire_hours or 24 app.set_key(key) asyncio.run(app.insert()) print(app_id)
async def test_verify_refresh_token_expired_token( fake_email, fake_refresh_client_app: ClientApp, monkeypatch, pwd_context, create_fake_queryset, ): uid = "fake_uuid" expires = datetime.datetime.now() + datetime.timedelta(hours=24) payload = { "iss": f"{config.ISSUER}/{fake_refresh_client_app.app_id}", "sub": fake_email, "uid": uid, } refresh_token = jwt.generate_jwt( payload, fake_refresh_client_app.get_refresh_key(), "ES256", datetime.timedelta(seconds=0), ) saved_refresh_token = RefreshToken( app_id=fake_refresh_client_app.app_id, email=fake_email, hash=pwd_context.hash(refresh_token), expires=expires, uid=uid, ) def _fake_query(*_args): return create_fake_queryset(get_return=saved_refresh_token) monkeypatch.setattr("app.security.token.RefreshToken.query", _fake_query) with pytest.raises(security_token.TokenVerificationError): await security_token.verify_refresh_token(refresh_token, fake_refresh_client_app)
def export_public_key(client_app: ClientApp) -> dict: return client_app.get_key().export_public(as_dict=True)
def verify(token: str, client_app: ClientApp) -> (dict, dict): return _check_token(token, client_app.get_key(), client_app.app_id)
async def delete_refresh_token(refresh_token: str, client_app: ClientApp): _, claims = _check_token(refresh_token, client_app.get_refresh_key(), client_app.app_id) found_rt = await _find_refresh_token(claims, client_app) await found_rt.delete()
async def check_refresh_client_app(client_app: ClientApp = Depends(check_client_app)): if not client_app.get_refresh_key(): raise HTTPException( status_code=403, detail="Refreshing isn't supported on this app" ) return client_app