async def test_verify_email_contact_method(self): async with op.session(self.appctx) as session: profile_id = await op.user_profile.\ create_user_profile('Jesse', 'Jesse Dhillon', '123foobar^#@', UserRole.Subscriber, '*****@*****.**', '+14155551234').\ execute(session) profile = await op.user_profile.\ get_user_profile(user_profile_id=profile_id).\ execute(session) body = { 'principalName': '*****@*****.**', 'principalType': 'email', } resp = await self.client.request('POST', '/authn/session', json=body) assert resp.status == 200 resp = await self.client.request( 'GET', f'/users/{profile.user_profile_id}/contactMethods/{profile.email_contact_method_id}/verify' ) j = await resp.json() assert resp.status == 200 assert j['data']['hint'] == 'j****@dhillon.com' session = self.get_session() vc = session['pending_challenge'] vcresp = {'challenge_id': vc['challenge_id'], 'passcode': vc['secret']} resp = await self.client.request( 'POST', f'/users/{profile.user_profile_id}/contactMethods/{profile.email_contact_method_id}/verify', json=vcresp) assert resp.status == 200 session = self.get_session() assert session['pending_challenge'] is None async with op.session(self.appctx) as session: cm = await op.user_profile.\ get_contact_method(profile.email_contact_method_id, user_profile_id=profile.user_profile_id).\ execute(session) assert cm.verified
async def get_contact_method_verify_challenge(request, ctx: AppConfig, session: AuthnSession): try: user_profile_id = UUID(request.match_info['user_profile_id']) contact_method_id = UUID(request.match_info['contact_method_id']) except ValueError: raise HTTPNotFound() if user_profile_id != session.user_profile_id: raise HTTPForbidden() async with op.session(ctx) as ss: cm = await op.user_profile.\ get_contact_method(contact_method_id, user_profile_id=user_profile_id).\ execute(ss) if cm.verified: raise ValueError("contact method is already verified") if cm.contact_method_type is ContactMethodType.Email: # TODO: deliver challenge chtype = AuthnChallengeType.Email elif cm.contact_method_type is ContactMethodType.Phone: # TODO: deliver challenge chtype = AuthnChallengeType.SMS session.require_challenge(chtype) session.next_challenge_for_contact_method(cm) return session.pending_challenge.get_view('public')
async def test_get_plan(self): async with op.session(self.appctx) as ss: plan_id = await op.membership.create_subscription_plan( rank=1, name="Basic member", description="- Ad-free podcast episodes\n" "- Access to episodes one week before non-subscribers\n" "- Monthly members-only episode\n", payment_demands=( (PaymentDemandType.Periodic, PaymentDemandPeriod.Quarterly, Decimal('25.0'), 'USD'), (PaymentDemandType.Periodic, PaymentDemandPeriod.Annually, Decimal('90.0'), 'USD'), (PaymentDemandType.Periodic, PaymentDemandPeriod.Monthly, Decimal('10.0'), 'USD'), (PaymentDemandType.Immediate, Decimal('250.0'), 'USD'))).\ execute(ss) assert plan_id is not None plans = await op.membership.\ get_subscription_plans().\ unmarshal_with(SubscriptionPlan).\ execute(ss) p = plans[0] assert len(plans) == 1 assert len(p.payment_demands) == 4
async def verify_contact_method(request, ctx: AppConfig, session: AuthnSession): try: user_profile_id = UUID(request.match_info['user_profile_id']) contact_method_id = UUID(request.match_info['contact_method_id']) except ValueError: raise HTTPNotFound() challenge_response = AuthnChallengeResponseRequest.unmarshal_request( await request.json()) if user_profile_id != session.user_profile_id: raise HTTPForbidden() if challenge_response.challenge_id != session.pending_challenge.challenge_id: raise HTTPBadRequest() session.pending_challenge.attempts += 1 session.changed() if session.pending_challenge.attempts > security.MaxVerificationChallengeAttempts: session.invalidate() raise HTTPForbidden(body="Too many invalid attempts") if challenge_response.passcode != session.pending_challenge.secret: raise HTTPUnauthorized(body="Incorrect passcode") async with op.session(ctx) as ss: await op.user_profile.\ mark_contact_method_verified(contact_method_id, user_profile_id=user_profile_id).\ execute(ss) session.clear_pending_challenge() return HTTPOk()
async def verify_authn_challenge(request, ctx: AppConfig, session: AuthnSession): if not session.pending_challenge: raise HTTPBadRequest() challenge_response =\ AuthnChallengeResponseRequest.unmarshal_request(await request.json()) if challenge_response.challenge_id != session.pending_challenge.challenge_id: raise ValueError("invalid challenge") session.pending_challenge.attempts += 1 session.changed() if session.pending_challenge.attempts > security.MaxVerificationChallengeAttempts: session.invalidate() raise HTTPForbidden(text="Too many invalid attempts") otp_types = [AuthnChallengeType.Email, AuthnChallengeType.SMS] if session.pending_challenge.challenge_type in otp_types: if challenge_response.passcode != session.pending_challenge.secret: raise HTTPUnauthorized(text="Incorrect passcode") elif session.pending_challenge.challenge_type is AuthnChallengeType.Password: async with op.session(ctx) as ss: result = await op.authn.\ authenticate_user( user_profile_id=session.user_profile_id, password=challenge_response.passcode).\ execute(ss) if not result: raise HTTPUnauthorized(text="Incorrect passcode") else: raise HTTPBadRequest() session.clear_pending_challenge() async with op.session(ctx) as ss: u = await op.user_profile.\ get_user_profile(user_profile_id=session.user_profile_id).\ execute(ss) if session.required_challenges: session.next_challenge_for_user(u) return HTTPAccepted(), session.pending_challenge.get_view('public') session.authenticated = True session.set_role(u.role) session.remove_capabilities(Capability.Authenticate) return HTTPOk()
async def test_get_authn_challenge(self): async with op.session(self.appctx) as session: profile_id = await op.user_profile.\ create_user_profile('Jesse', 'Jesse Dhillon', '123foobar^#@', UserRole.Subscriber, '*****@*****.**', '+14155551234').\ execute(session) profile = await op.user_profile.\ get_user_profile(user_profile_id=profile_id).\ execute(session) await op.user_profile.\ mark_contact_method_verified(contact_method_id=profile.email_contact_method_id).\ execute(session) body = { 'principalName': '*****@*****.**', 'principalType': 'email', } await self.client.request('POST', '/authn/session', json=body) await self.client.request('GET', '/authn/challenge') session = self.get_session() assert session['pending_challenge'] is not None challenge = session['pending_challenge'] chresp = { 'challenge_id': challenge['challenge_id'], 'passcode': challenge['secret'], } resp = await self.client.request('POST', '/authn/challenge', json={ **chresp, 'passcode': 'foobar' }) assert resp.status == 401 j = await resp.json() assert not j['status']['success'] assert 'Incorrect passcode' in [ e['message'] for e in j['status']['errors'] ] session = self.get_session() challenge = session['pending_challenge'] assert challenge['attempts'] == 1 resp = await self.client.request('POST', '/authn/challenge', json=chresp) session = self.get_session() assert resp.status == 200 # check that we have some expected normie permissions assert set(session['capabilities']) >= set( ['profile.list', 'payment_method.create']) resp = await self.client.request('POST', '/authn/challenge', json=chresp) assert resp.status == 403
async def get_subscription_plans(request, ctx: AppConfig, session: AuthnSession): async with op.session(ctx) as ss: plans = await op.membership.\ get_subscription_plans().\ unmarshal_with(SubscriptionPlan).\ execute(ss) return plans.get_view('default')
async def get_authn_challenge(request, ctx: AppConfig, session: AuthnSession): if session.pending_challenge: raise HTTPBadRequest( "cannot request a new challenge while challenges are pending") async with op.session(ctx) as ss: u = await op.user_profile.\ get_user_profile(user_profile_id=session.user_profile_id).\ execute(ss) if session.required_challenges: session.next_challenge_for_user(u) return session.pending_challenge.get_view('public')
async def test_add_multiple_payment_method(self): profile_id = await self.authenticate_as(UserRole.Subscriber) bodies = [{ 'processorId': 'com.example', 'paymentCredential': { 'methodType': 'credit_card', 'cardNumber': '4242424242424242', 'expMonth': 12, 'expYear': 2023, 'cvv': '444', } }, { 'processorId': 'com.example', 'paymentCredential': { 'methodType': 'credit_card', 'cardNumber': '4000056655665556', 'expMonth': 12, 'expYear': 2022, 'cvv': '555', } }] pm_ids = [] for b in bodies: resp = await self.client.request( 'POST', f'/users/{profile_id}/paymentMethods', json=b) j = await resp.json() pm_ids.append(j['data']) payments = self.appctx.payments.get() mock = payments['com.example'] async with op.session(self.appctx) as ss: pp = await op.user_profile.get_payment_profile( user_profile_id=profile_id, processor_id='com.example').\ execute(ss) pms = await op.user_profile.get_payment_methods( user_profile_id=profile_id, payment_profile_id=pp.payment_profile_id).\ execute(ss) assert pp.processor_customer_profile_id in mock._customers for pp_id, pmeths in pms.group_by('payment_profile_id').items(): assert pp_id == pp.payment_profile_id assert len(pmeths) == 2 for pm in pmeths: assert str(pm.payment_method_id) in pm_ids
async def test_cannot_get_authn_challenge_with_unverified_email(self): async with op.session(self.appctx) as session: profile_id = await op.user_profile.create_user_profile( 'Jesse', 'Jesse Dhillon', '123foobar^#@', UserRole.Subscriber, '*****@*****.**', '+14155551234').execute(session) body = { 'principalName': '*****@*****.**', 'principalType': 'email', } await self.client.request('POST', '/authn/session', json=body) resp = await self.client.request('GET', '/authn/challenge') j = await resp.json() assert resp.status == 400 assert "email j****@dhillon.com must be verified first" in\ [e['message'] for e in j['status']['errors']]
async def authenticate_as(self, role): async with op.session(self.appctx) as session: profile_id = await op.user_profile.\ create_user_profile('Test', 'Test User', '123foobar^#@', role, '*****@*****.**', '+14155551234').\ execute(session) profile = await op.user_profile.\ get_user_profile(user_profile_id=profile_id).\ execute(session) await op.user_profile.\ mark_contact_method_verified(contact_method_id=profile.email_contact_method_id).\ execute(session) body = { 'principalName': profile.email_address, 'principalType': 'email', } await self.client.request('POST', '/authn/session', json=body) await self.client.request('GET', '/authn/challenge') cookie = json.loads(self.get_cookie('TEST_SESSION').value) session = cookie['session'] challenge = session['pending_challenge'] chresp = { 'challenge_id': challenge['challenge_id'], 'passcode': challenge['secret'], } await self.client.request('POST', '/authn/challenge', json=chresp) cookie = json.loads(self.get_cookie('TEST_SESSION').value) session = cookie['session'] if session['pending_challenge'] is not None: challenge = session['pending_challenge'] chresp = { 'challenge_id': challenge['challenge_id'], 'passcode': '123foobar^#@', } await self.client.request('POST', '/authn/challenge', json=chresp) return profile_id
async def test_start_session(self): async with op.session(self.appctx) as session: profile_id = await op.user_profile.\ create_user_profile('Jesse', 'Jesse Dhillon', '123foobar^#@', UserRole.Subscriber, '*****@*****.**', '+14155551234').\ execute(session) body = { 'principalName': '*****@*****.**', 'principalType': 'email', } resp = await self.client.request('POST', '/authn/session', json=body) j = await resp.json() assert 'TEST_SESSION' in resp.cookies session = self.get_session() assert 'capabilities' in session assert set(session['capabilities']) == set( [Capability.Authenticate.value])
async def create_subscription_plan(request, ctx: AppConfig, session: AuthnSession): plan = CreateSubscriptionPlanRequest.unmarshal_request(await request.json()) async with op.session(ctx) as ss: pds = [] for pd in plan.payment_demands: if pd.demand_type is PaymentDemandType.Periodic: pds.append((pd.demand_type, pd.period, pd.amount, pd.iso_currency, pd.non_iso_currency)) elif pd.demand_type is PaymentDemandType.Immediate: pds.append((pd.demand_type, pd.amount, pd.iso_currency, pd.non_iso_currency)) else: raise ValueError(pd.demand_type) plan_id = await op.membership.\ create_subscription_plan(rank=plan.rank, name=plan.name, description=plan.description, payment_demands=pds).\ execute(ss) return plan_id
async def test_create_and_get_plan(self): async with op.session(self.appctx) as ss: plan_id = await op.membership.create_subscription_plan( rank=1, name="Basic member", description="- Ad-free podcast episodes\n" "- Access to episodes one week before non-subscribers\n" "- Monthly members-only episode\n", payment_demands=( (PaymentDemandType.Periodic, PaymentDemandPeriod.Quarterly, Decimal('25.0'), 'USD'), (PaymentDemandType.Periodic, PaymentDemandPeriod.Annually, Decimal('90.0'), 'USD'), (PaymentDemandType.Periodic, PaymentDemandPeriod.Monthly, Decimal('10.0'), 'USD'), (PaymentDemandType.Immediate, Decimal('250.0'), 'USD'))).\ execute(ss) assert plan_id is not None plans = await op.membership.get_subscription_plans().execute(ss) assert len(plans) == 4 assert plans[0].demand_type is PaymentDemandType.Periodic assert plans[1].demand_type is PaymentDemandType.Periodic assert plans[2].demand_type is PaymentDemandType.Periodic assert plans[3].demand_type is PaymentDemandType.Immediate assert plans[0].period is PaymentDemandPeriod.Monthly assert plans[1].period is PaymentDemandPeriod.Quarterly assert plans[2].period is PaymentDemandPeriod.Annually assert plans[3].period is None assert plans[0].amount == Decimal('10.0') assert plans[1].amount == Decimal('25.0') assert plans[2].amount == Decimal('90.0') assert plans[3].amount == Decimal('250.0') s = {p.subscription_plan_id for p in plans} assert len(s) == 1
async def init_authn_session(request, ctx: AppConfig, session: AuthnSession): sr = InitiateAuthnSessionRequest.unmarshal_request(await request.json()) async with op.session(ctx) as ss: if sr.principal_type is AuthnPrincipalType.Email: u = await op.user_profile.get_user_profile( email_address=sr.principal_name).execute(ss) session.require_challenge(AuthnChallengeType.Email) elif sr.principal_type is AuthnPrincipalType.Phone: u = await op.user_profile.get_user_profile( phone_number=sr.principal_name).execute(ss) session.require_challenge(AuthnChallengeType.SMS) else: raise ValueError(sr.principal_type) if u is None: raise HTTPUnauthorized() session.user_profile_id = u.user_profile_id if u.role in {UserRole.Superuser, UserRole.Manager, UserRole.Creator}: session.require_challenge(AuthnChallengeType.Password) session.add_capabilities(Capability.Authenticate) return HTTPOk()
async def add_payment_method(request, ctx: AppConfig, session: AuthnSession): try: user_profile_id = UUID(request.match_info['user_profile_id']) except ValueError: raise HTTPNotFound() if user_profile_id != session.user_profile_id: raise HTTPForbidden() async with op.session(ctx) as ss: # fail if user's email address is not verified u = await op.user_profile.get_user_profile( user_profile_id=user_profile_id).execute(ss) if not u.email_contact_method_verified: raise ValueError("email address must be verified first") addpmrq = AddPaymentMethodRequest.unmarshal_request(await request.json()) payments = ctx.payments.get() processor = payments[addpmrq.processor_id] pp = await op.user_profile.\ get_payment_profile(user_profile_id=user_profile_id, processor_id=addpmrq.processor_id).\ execute(ss) if pp is not None: pp_id = pp.payment_profile_id cust_id = pp.processor_customer_profile_id else: cust_id = await processor.create_customer_profile( u.user_profile_id, u.name, u.email_address, u.phone_number, address=None) pp_id = await op.user_profile.\ add_payment_profile( user_profile_id=u.user_profile_id, processor_id=processor.processor_id, processor_customer_profile_id=cust_id).\ execute(ss) async with op.session(ctx) as ss: method_id = await processor.\ add_customer_payment_method(cust_id, addpmrq.payment_credential) pm_id = await op.user_profile.\ add_payment_method( user_profile_id=u.user_profile_id, payment_profile_id=pp_id, processor_payment_method_id=method_id, payment_method_type=addpmrq.payment_credential.method_type, payment_method_family=addpmrq.payment_credential.method_family, display_name=addpmrq.payment_credential.display_name, safe_account_number_fragment=\ addpmrq.payment_credential.safe_account_number_fragment, expires_after=addpmrq.payment_credential.expire_after_date).\ execute(ss) return pm_id
async def create_subscription(request, ctx: AppConfig, session: AuthnSession): subscription_plan_id = request.match_info['subscription_plan_id'] subreq = CreateSubscriptionRequest.unmarshal_request(await request.json()) async with op.session(ctx) as ss: # TODO: just use and expand the get_payment_profile method profiles = await op.user_profile.\ get_payment_methods(user_profile_id=session.user_profile_id, payment_method_id=subreq.payment_method_id).\ unmarshal_with(PaymentProfile).\ execute(ss) pp = profiles[0] pm = pp.payment_methods.find( payment_method_id=subreq.payment_method_id) plan = await op.membership.\ get_subscription_plan(subscription_plan_id=subreq.subscription_plan_id).\ unmarshal_with(SubscriptionPlan).\ execute(ss) pd = plan.payment_demands.find( payment_demand_id=subreq.payment_demand_id) payments = ctx.payments.get() processor = payments[pp.processor_id] if pd.demand_type is PaymentDemandType.Periodic: charge_id = await processor.create_recurring_charge( vocal_user_profile_id=session.user_profile_id, customer_profile_id=pp.processor_customer_profile_id, payment_method_id=pm.processor_payment_method_id, start_date=datetime.today(), period=pd.period, amount=pd.amount, iso_currency=pd.iso_currency, non_iso_currency=pd.non_iso_currency) return await op.membership.\ create_subscription( user_profile_id=session.user_profile_id, subscription_plan_id=plan.subscription_plan_id, payment_demand_id=pd.payment_demand_id, payment_profile_id=pp.payment_profile_id, payment_method_id=pm.payment_method_id, processor_charge_id=charge_id).\ unmarshal_with(Subscription).\ execute(ss) elif pd.demand_type is PaymentDemandType.Immediate: charge_id = await processor.create_immediate_charge( vocal_user_profile_id=session.user_profile_id, customer_profile_id=pp.processor_customer_profile_id, payment_method_id=pm.processor_payment_method_id, amount=pd.amount, iso_currency=pd.iso_currency, non_iso_currency=pd.non_iso_currency) return await op.membership.\ create_subscription( user_profile_id=session.user_profile_id, subscription_plan_id=plan.subscription_plan_id, payment_demand_id=pd.payment_demand_id, payment_profile_id=pp.payment_profile_id, payment_method_id=pm.payment_method_id, processor_charge_id=charge_id).\ unmarshal_with(Subscription).\ execute(ss) else: raise ValueError(pd.demand_type)