From 51b264ca13fffc66e2dc31e87b0934ba61a48435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Mon, 3 Apr 2023 15:43:25 +0200 Subject: [PATCH] feat(auth): feature entitlement check for u2f endpoints --- .pnp.cjs | 14 ++-- ...res-npm-1.58.12-9778b78276-3fcd9a9488.zip} | Bin 26973 -> 27102 bytes packages/auth/package.json | 2 +- packages/auth/src/Bootstrap/Container.ts | 20 +++++- .../Controller/AuthenticatorsController.ts | 22 +++++++ .../src/Domain/Feature/FeatureService.spec.ts | 59 ++++++++++++++++- .../auth/src/Domain/Feature/FeatureService.ts | 19 ++++++ .../Domain/Feature/FeatureServiceInterface.ts | 1 + .../DeleteAuthenticator.spec.ts | 48 +++++++++++++- .../DeleteAuthenticator.ts | 35 +++++++++- ...teAuthenticatorRegistrationOptions.spec.ts | 43 ++++++++++++ ...enerateAuthenticatorRegistrationOptions.ts | 19 ++++++ .../ListAuthenticators.spec.ts | 33 +++++++++- .../ListAuthenticators/ListAuthenticators.ts | 24 ++++++- ...yAuthenticatorRegistrationResponse.spec.ts | 61 ++++++++++++++++++ ...VerifyAuthenticatorRegistrationResponse.ts | 19 ++++++ yarn.lock | 10 +-- 17 files changed, 407 insertions(+), 22 deletions(-) rename .yarn/cache/{@standardnotes-features-npm-1.58.9-c278f712cd-218350ee55.zip => @standardnotes-features-npm-1.58.12-9778b78276-3fcd9a9488.zip} (68%) diff --git a/.pnp.cjs b/.pnp.cjs index 1b1011f72..9765822e4 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -4175,7 +4175,7 @@ const RAW_RUNTIME_STATE = ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\ - ["@standardnotes/features", "npm:1.58.9"],\ + ["@standardnotes/features", "npm:1.58.12"],\ ["@standardnotes/predicates", "workspace:packages/predicates"],\ ["@standardnotes/responses", "npm:1.13.9"],\ ["@standardnotes/security", "workspace:packages/security"],\ @@ -4360,10 +4360,10 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@standardnotes/features", [\ - ["npm:1.58.8", {\ - "packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.58.8-d97ff2aae1-77bac7d0a0.zip/node_modules/@standardnotes/features/",\ + ["npm:1.58.12", {\ + "packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.58.12-9778b78276-3fcd9a9488.zip/node_modules/@standardnotes/features/",\ "packageDependencies": [\ - ["@standardnotes/features", "npm:1.58.8"],\ + ["@standardnotes/features", "npm:1.58.12"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/security", "workspace:packages/security"],\ @@ -4371,10 +4371,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["npm:1.58.9", {\ - "packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.58.9-c278f712cd-218350ee55.zip/node_modules/@standardnotes/features/",\ + ["npm:1.58.8", {\ + "packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.58.8-d97ff2aae1-77bac7d0a0.zip/node_modules/@standardnotes/features/",\ "packageDependencies": [\ - ["@standardnotes/features", "npm:1.58.9"],\ + ["@standardnotes/features", "npm:1.58.8"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/security", "workspace:packages/security"],\ diff --git a/.yarn/cache/@standardnotes-features-npm-1.58.9-c278f712cd-218350ee55.zip b/.yarn/cache/@standardnotes-features-npm-1.58.12-9778b78276-3fcd9a9488.zip similarity index 68% rename from .yarn/cache/@standardnotes-features-npm-1.58.9-c278f712cd-218350ee55.zip rename to .yarn/cache/@standardnotes-features-npm-1.58.12-9778b78276-3fcd9a9488.zip index 8237ee66443541d266501460b05210f8b9601667..56c5d52314f67f02066b061b0e65fe3592ecab95 100644 GIT binary patch delta 5152 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
gW(G>e-Q#0($y#dBH%6TY zi1!turW>+=4bPze7jK4X31dG03;It+Mp4;Q*g-eME%~_G0~QT-IL{a3u~yZU7gGJv z>*aK#q^Eth29Y6yxMI1rEYcf_1Z_N+$cjTW2`2RCrCTjFC%$pm~TSuzojv{XnMF*SdE$@hMDuX z%#S0Kz%qY%yN)#}jzAEEEz~Z>wo|sB-po<^5Qo+A(g|OpTR#Oxndl!O7^{iLoh=m|ga^zvWSE2y+$6WU9LHl*CP_%NJ* z3WZc!6u2eGz?>5vGC1v%`%LW-PoDBz4j?HU~DrOv{>unY9^3*1kcfT*m z{AdB9Us(RI$)kHXh}XNBHN@97n7!86x>kwY_c+`b@abNs6*Wc*l$DjaZ)Zdt3^AO4 zP&E%O7s%Uo93AR(xca4)LzHdOofuiQ@Uf}n0T?FWD0JkFyiWQnC}mBGvbQ0l(sW>F ziI?bC>@dfy*me>joqTx#A4R&(Ky9~1Rt9@qNN~(|{N%#X4m$bQ7EXStj_hAP=)I@* zc}`S#;xn`mC%>D!_)?&lVU~Y3J)t?_HsLTWlM+;54pM^Wq7d5HR$Kb zrB2?oJ89$CK}Kwz96ri7ge8y7#Fr4y_JA|HACY$2ftd1{QsBTg(!a$PsU3h#Wi65V zVx0&&3B*p!P?kFU~X+Mt*-E+4JjZalLHZM*xzmOFp+o9aETB zd|XiVgKuL(B=A!4!1mOtdcEdFM7+d~UPCK)n+=cW%)% z1xG7GPoV1sPulDd)t5))0t4*IiRVwk4{tg~6$Mb#$&lxj`Y`tD7H6V*LbB zP~Ms&B>i{jj^l*8i7ZP8O8pmQhLdl$w!vV4^UJLwA%vkPp9y@y_Ok7U9(A-JJiJb=CBn|g`d+}!ee#@d86TLG?N;OiMjm1VFmpce4|ae>P!8bZfB+5VJm722 zTdO?4^n@F1%5^(@2&6tChyL$369m!${qHFZBj5%)o>)QuAk+V|g9^i5^4$Tt<<`J^ zBSvDaKa6%jt%X0BR_M-!LL34-$D5#(B$w#KA3Mw;>M!1uFqCLL6X` zQUij;A%LU}3Gh~egy26xg<;zgcg8&lJV4f(2V5?I9A?;rr)Q@c}*sBr^K4kSRU z104nxpy70hFIoV3)xEN%1d&;8ef> delta 5030 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.ListAuthenticators) - .toConstantValue(new ListAuthenticators(container.get(TYPES.AuthenticatorRepository))) + .toConstantValue( + new ListAuthenticators( + container.get(TYPES.AuthenticatorRepository), + container.get(TYPES.UserRepository), + container.get(TYPES.FeatureService), + ), + ) container .bind(TYPES.DeleteAuthenticator) - .toConstantValue(new DeleteAuthenticator(container.get(TYPES.AuthenticatorRepository))) + .toConstantValue( + new DeleteAuthenticator( + container.get(TYPES.AuthenticatorRepository), + container.get(TYPES.UserRepository), + container.get(TYPES.FeatureService), + ), + ) container .bind(TYPES.GenerateRecoveryCodes) .toConstantValue( diff --git a/packages/auth/src/Controller/AuthenticatorsController.ts b/packages/auth/src/Controller/AuthenticatorsController.ts index cfbf9da40..ad13b7096 100644 --- a/packages/auth/src/Controller/AuthenticatorsController.ts +++ b/packages/auth/src/Controller/AuthenticatorsController.ts @@ -34,6 +34,17 @@ export class AuthenticatorsController { userUuid: params.userUuid, }) + if (result.isFailed()) { + return { + status: HttpStatusCode.Unauthorized, + data: { + error: { + message: result.getError(), + }, + }, + } + } + return { status: HttpStatusCode.Success, data: { @@ -50,6 +61,17 @@ export class AuthenticatorsController { authenticatorId: params.authenticatorId, }) + if (result.isFailed()) { + return { + status: HttpStatusCode.Unauthorized, + data: { + error: { + message: result.getError(), + }, + }, + } + } + return { status: HttpStatusCode.Success, data: { diff --git a/packages/auth/src/Domain/Feature/FeatureService.spec.ts b/packages/auth/src/Domain/Feature/FeatureService.spec.ts index 4f96470e7..3edad38d8 100644 --- a/packages/auth/src/Domain/Feature/FeatureService.spec.ts +++ b/packages/auth/src/Domain/Feature/FeatureService.spec.ts @@ -30,7 +30,7 @@ jest.mock('@standardnotes/features', () => { const { GetFeatures } = jest.requireMock('@standardnotes/features') import { FeatureService } from './FeatureService' -import { Permission, PermissionName } from '@standardnotes/features' +import { FeatureIdentifier, Permission, PermissionName } from '@standardnotes/features' import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface' import { TimerInterface } from '@standardnotes/time' import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription' @@ -201,6 +201,62 @@ describe('FeatureService', () => { }) describe('online subscribers', () => { + it('should tell if a user is entitled to a feature', async () => { + expect(await createService().userIsEntitledToFeature(user, FeatureIdentifier.AutobiographyTheme)).toBe(true) + expect(await createService().userIsEntitledToFeature(user, FeatureIdentifier.DeprecatedBoldEditor)).toBe(false) + }) + + it('should tell if a user is not entitled to a feature because it is expired', async () => { + timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(777) + expect(await createService().userIsEntitledToFeature(user, FeatureIdentifier.AutobiographyTheme)).toBe(false) + }) + + it('should tell if a user is entitled to a feature that does not expire', async () => { + const nonSubscriptionPermission = { + uuid: 'files-beta-permission-1-1-1', + name: PermissionName.FilesBeta, + } as jest.Mocked + + GetFeatures.mockImplementation(() => [ + { + identifier: 'org.standardnotes.theme-autobiography', + permission_name: PermissionName.AutobiographyTheme, + expires_at: 555, + }, + { + identifier: 'org.standardnotes.bold-editor', + permission_name: PermissionName.BoldEditor, + expires_at: 777, + }, + { + identifier: 'files-beta', + permission_name: PermissionName.FilesBeta, + expires_at: undefined, + no_expire: true, + }, + ]) + + const nonSubscriptionRole = { + name: RoleName.NAMES.InternalTeamUser, + uuid: 'role-files-beta', + permissions: Promise.resolve([nonSubscriptionPermission]), + } as jest.Mocked + + roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([nonSubscriptionRole]) + roleToSubscriptionMap.getSubscriptionNameForRoleName = jest + .fn() + .mockReturnValueOnce(SubscriptionName.PlusPlan) + .mockReturnValueOnce(SubscriptionName.ProPlan) + + user = { + uuid: 'user-1-1-1', + roles: Promise.resolve([role1, role2, nonSubscriptionRole]), + subscriptions: Promise.resolve([subscription1, subscription2]), + } as jest.Mocked + + expect(await createService().userIsEntitledToFeature(user, 'files-beta')).toBe(true) + }) + it('should return user features with `expires_at` field', async () => { const features = await createService().getFeaturesForUser(user) expect(features).toEqual( @@ -336,6 +392,7 @@ describe('FeatureService', () => { expires_at: 777, }, { + identifier: 'files-beta', permission_name: PermissionName.FilesBeta, expires_at: undefined, no_expire: true, diff --git a/packages/auth/src/Domain/Feature/FeatureService.ts b/packages/auth/src/Domain/Feature/FeatureService.ts index 6850c5edb..92b27bf9f 100644 --- a/packages/auth/src/Domain/Feature/FeatureService.ts +++ b/packages/auth/src/Domain/Feature/FeatureService.ts @@ -21,6 +21,25 @@ export class FeatureService implements FeatureServiceInterface { @inject(TYPES.Timer) private timer: TimerInterface, ) {} + async userIsEntitledToFeature(user: User, featureIdentifier: string): Promise { + const userFeatures = await this.getFeaturesForUser(user) + + const feature = userFeatures.find((userFeature) => userFeature.identifier === featureIdentifier) + + if (feature === undefined) { + return false + } + + if (feature.no_expire) { + return true + } + + const featureIsExpired = + feature.expires_at !== undefined && feature.expires_at < this.timer.getTimestampInMicroseconds() + + return !featureIsExpired + } + async getFeaturesForOfflineUser(email: string): Promise<{ features: FeatureDescription[]; roles: string[] }> { const userSubscriptions = await this.offlineUserSubscriptionRepository.findByEmail( email, diff --git a/packages/auth/src/Domain/Feature/FeatureServiceInterface.ts b/packages/auth/src/Domain/Feature/FeatureServiceInterface.ts index 551687d13..379dc256f 100644 --- a/packages/auth/src/Domain/Feature/FeatureServiceInterface.ts +++ b/packages/auth/src/Domain/Feature/FeatureServiceInterface.ts @@ -4,5 +4,6 @@ import { User } from '../User/User' export interface FeatureServiceInterface { getFeaturesForUser(user: User): Promise> + userIsEntitledToFeature(user: User, featureIdentifier: string): Promise getFeaturesForOfflineUser(email: string): Promise<{ features: FeatureDescription[]; roles: string[] }> } diff --git a/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.spec.ts b/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.spec.ts index a67e48971..0ecaebd14 100644 --- a/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.spec.ts +++ b/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.spec.ts @@ -2,12 +2,18 @@ import { Dates, Uuid } from '@standardnotes/domain-core' import { Authenticator } from '../../Authenticator/Authenticator' import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' +import { FeatureServiceInterface } from '../../Feature/FeatureServiceInterface' +import { User } from '../../User/User' +import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { DeleteAuthenticator } from './DeleteAuthenticator' describe('DeleteAuthenticator', () => { let authenticatorRepository: AuthenticatorRepositoryInterface let authenticator: Authenticator - const createUseCase = () => new DeleteAuthenticator(authenticatorRepository) + let userRepository: UserRepositoryInterface + let featureService: FeatureServiceInterface + + const createUseCase = () => new DeleteAuthenticator(authenticatorRepository, userRepository, featureService) beforeEach(() => { authenticator = Authenticator.create({ @@ -24,6 +30,12 @@ describe('DeleteAuthenticator', () => { authenticatorRepository = {} as jest.Mocked authenticatorRepository.findById = jest.fn().mockReturnValue(authenticator) authenticatorRepository.remove = jest.fn() + + userRepository = {} as jest.Mocked + userRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked) + + featureService = {} as jest.Mocked + featureService.userIsEntitledToFeature = jest.fn().mockReturnValue(true) }) it('should return error if authenticator not found', async () => { @@ -38,6 +50,40 @@ describe('DeleteAuthenticator', () => { expect(result.getError()).toEqual('Authenticator not found') }) + it('should return error if user is not found', async () => { + userRepository.findOneByUuid = jest.fn().mockReturnValue(null) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + authenticatorId: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toEqual('Could not delete authenticator: user not found.') + }) + + it('should return error if user is not entitled to U2F', async () => { + featureService.userIsEntitledToFeature = jest.fn().mockReturnValue(false) + + const result = await createUseCase().execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + authenticatorId: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toEqual('Could not delete authenticator: user is not entitled to U2F.') + }) + + it('should return error if user uuid is not valid', async () => { + const result = await createUseCase().execute({ + userUuid: 'invalid', + authenticatorId: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toEqual('Could not delete authenticator: Given value is not a valid uuid: invalid') + }) + it('should return error if authenticator does not belong to user', async () => { authenticatorRepository.findById = jest.fn().mockReturnValue({ ...authenticator, diff --git a/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.ts b/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.ts index 9c61fa273..45fdc313e 100644 --- a/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.ts +++ b/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.ts @@ -1,12 +1,41 @@ -import { Result, UniqueEntityId, UseCaseInterface } from '@standardnotes/domain-core' +import { Result, UniqueEntityId, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { FeatureIdentifier } from '@standardnotes/features' + import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' +import { FeatureServiceInterface } from '../../Feature/FeatureServiceInterface' +import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { DeleteAuthenticatorDTO } from './DeleteAuthenticatorDTO' export class DeleteAuthenticator implements UseCaseInterface { - constructor(private authenticatorRepository: AuthenticatorRepositoryInterface) {} + constructor( + private authenticatorRepository: AuthenticatorRepositoryInterface, + private userRepository: UserRepositoryInterface, + private featureService: FeatureServiceInterface, + ) {} + async execute(dto: DeleteAuthenticatorDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(`Could not delete authenticator: ${userUuidOrError.getError()}`) + } + const userUuid = userUuidOrError.getValue() + + const user = await this.userRepository.findOneByUuid(userUuid.value) + if (user === null) { + return Result.fail('Could not delete authenticator: user not found.') + } + + const userIsEntitledToU2F = await this.featureService.userIsEntitledToFeature( + user, + FeatureIdentifier.UniversalSecondFactor, + ) + + if (!userIsEntitledToU2F) { + return Result.fail('Could not delete authenticator: user is not entitled to U2F.') + } + const authenticator = await this.authenticatorRepository.findById(new UniqueEntityId(dto.authenticatorId)) - if (!authenticator || authenticator.props.userUuid.value !== dto.userUuid) { + if (!authenticator || authenticator.props.userUuid.value !== userUuid.value) { return Result.fail('Authenticator not found') } diff --git a/packages/auth/src/Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions.spec.ts b/packages/auth/src/Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions.spec.ts index 17ebac431..c353739da 100644 --- a/packages/auth/src/Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions.spec.ts +++ b/packages/auth/src/Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions.spec.ts @@ -4,11 +4,16 @@ import { Authenticator } from '../../Authenticator/Authenticator' import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge' import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface' import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' +import { FeatureServiceInterface } from '../../Feature/FeatureServiceInterface' +import { User } from '../../User/User' +import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { GenerateAuthenticatorRegistrationOptions } from './GenerateAuthenticatorRegistrationOptions' describe('GenerateAuthenticatorRegistrationOptions', () => { let authenticatorRepository: AuthenticatorRepositoryInterface let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface + let userRepository: UserRepositoryInterface + let featureService: FeatureServiceInterface const createUseCase = () => new GenerateAuthenticatorRegistrationOptions( @@ -16,6 +21,8 @@ describe('GenerateAuthenticatorRegistrationOptions', () => { authenticatorChallengeRepository, 'Standard Notes', 'standardnotes.com', + userRepository, + featureService, ) beforeEach(() => { @@ -35,6 +42,12 @@ describe('GenerateAuthenticatorRegistrationOptions', () => { authenticatorChallengeRepository = {} as jest.Mocked authenticatorChallengeRepository.save = jest.fn() + + userRepository = {} as jest.Mocked + userRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked) + + featureService = {} as jest.Mocked + featureService.userIsEntitledToFeature = jest.fn().mockReturnValue(true) }) it('should return error if userUuid is invalid', async () => { @@ -63,6 +76,36 @@ describe('GenerateAuthenticatorRegistrationOptions', () => { expect(result.getError()).toBe('Could not generate authenticator registration options: Username cannot be empty') }) + it('should return error if user is not entitled to u2f feature', async () => { + featureService.userIsEntitledToFeature = jest.fn().mockReturnValue(false) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + username: 'username', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe( + 'Could not generate authenticator registration options: user is not entitled to U2F.', + ) + }) + + it('should return error if user is not found', async () => { + userRepository.findOneByUuid = jest.fn().mockReturnValue(null) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + username: 'username', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Could not generate authenticator registration options: user not found.') + }) + it('should return error if authenticator challenge is invalid', async () => { const mock = jest.spyOn(AuthenticatorChallenge, 'create') mock.mockReturnValue(Result.fail('Oops')) diff --git a/packages/auth/src/Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions.ts b/packages/auth/src/Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions.ts index cc2ca9fc4..b24b211e3 100644 --- a/packages/auth/src/Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions.ts +++ b/packages/auth/src/Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions.ts @@ -5,6 +5,9 @@ import { GenerateAuthenticatorRegistrationOptionsDTO } from './GenerateAuthentic import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface' import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge' +import { FeatureIdentifier } from '@standardnotes/features' +import { FeatureServiceInterface } from '../../Feature/FeatureServiceInterface' +import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' export class GenerateAuthenticatorRegistrationOptions implements UseCaseInterface> { constructor( @@ -12,6 +15,8 @@ export class GenerateAuthenticatorRegistrationOptions implements UseCaseInterfac private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface, private relyingPartyName: string, private relyingPartyId: string, + private userRepository: UserRepositoryInterface, + private featureService: FeatureServiceInterface, ) {} async execute(dto: GenerateAuthenticatorRegistrationOptionsDTO): Promise>> { @@ -27,6 +32,20 @@ export class GenerateAuthenticatorRegistrationOptions implements UseCaseInterfac } const username = usernameOrError.getValue() + const user = await this.userRepository.findOneByUuid(userUuid.value) + if (user === null) { + return Result.fail('Could not generate authenticator registration options: user not found.') + } + + const userIsEntitledToU2F = await this.featureService.userIsEntitledToFeature( + user, + FeatureIdentifier.UniversalSecondFactor, + ) + + if (!userIsEntitledToU2F) { + return Result.fail('Could not generate authenticator registration options: user is not entitled to U2F.') + } + const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid) const options = generateRegistrationOptions({ rpID: this.relyingPartyId, diff --git a/packages/auth/src/Domain/UseCase/ListAuthenticators/ListAuthenticators.spec.ts b/packages/auth/src/Domain/UseCase/ListAuthenticators/ListAuthenticators.spec.ts index af6b0953a..5cf2166bf 100644 --- a/packages/auth/src/Domain/UseCase/ListAuthenticators/ListAuthenticators.spec.ts +++ b/packages/auth/src/Domain/UseCase/ListAuthenticators/ListAuthenticators.spec.ts @@ -1,14 +1,25 @@ import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' +import { FeatureServiceInterface } from '../../Feature/FeatureServiceInterface' +import { User } from '../../User/User' +import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { ListAuthenticators } from './ListAuthenticators' describe('ListAuthenticators', () => { let authenticatorRepository: AuthenticatorRepositoryInterface + let userRepository: UserRepositoryInterface + let featureService: FeatureServiceInterface - const createUseCase = () => new ListAuthenticators(authenticatorRepository) + const createUseCase = () => new ListAuthenticators(authenticatorRepository, userRepository, featureService) beforeEach(() => { authenticatorRepository = {} as jest.Mocked authenticatorRepository.findByUserUuid = jest.fn().mockReturnValue([]) + + userRepository = {} as jest.Mocked + userRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked) + + featureService = {} as jest.Mocked + featureService.userIsEntitledToFeature = jest.fn().mockReturnValue(true) }) it('should list authenticators', async () => { @@ -27,4 +38,24 @@ describe('ListAuthenticators', () => { expect(result.isFailed()).toBeTruthy() }) + + it('should fail if user is not found', async () => { + userRepository.findOneByUuid = jest.fn().mockReturnValue(null) + + const useCase = createUseCase() + + const result = await useCase.execute({ userUuid: '00000000-0000-0000-0000-000000000000' }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should fail if user is not entitled to U2F', async () => { + featureService.userIsEntitledToFeature = jest.fn().mockReturnValue(false) + + const useCase = createUseCase() + + const result = await useCase.execute({ userUuid: '00000000-0000-0000-0000-000000000000' }) + + expect(result.isFailed()).toBeTruthy() + }) }) diff --git a/packages/auth/src/Domain/UseCase/ListAuthenticators/ListAuthenticators.ts b/packages/auth/src/Domain/UseCase/ListAuthenticators/ListAuthenticators.ts index 6e14fdf52..6e4521df5 100644 --- a/packages/auth/src/Domain/UseCase/ListAuthenticators/ListAuthenticators.ts +++ b/packages/auth/src/Domain/UseCase/ListAuthenticators/ListAuthenticators.ts @@ -1,11 +1,19 @@ import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { FeatureIdentifier } from '@standardnotes/features' import { Authenticator } from '../../Authenticator/Authenticator' import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' +import { FeatureServiceInterface } from '../../Feature/FeatureServiceInterface' +import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { ListAuthenticatorsDTO } from './ListAuthenticatorsDTO' export class ListAuthenticators implements UseCaseInterface { - constructor(private authenticatorRepository: AuthenticatorRepositoryInterface) {} + constructor( + private authenticatorRepository: AuthenticatorRepositoryInterface, + private userRepository: UserRepositoryInterface, + private featureService: FeatureServiceInterface, + ) {} + async execute(dto: ListAuthenticatorsDTO): Promise> { const userUuidOrError = Uuid.create(dto.userUuid) if (userUuidOrError.isFailed()) { @@ -13,6 +21,20 @@ export class ListAuthenticators implements UseCaseInterface { } const userUuid = userUuidOrError.getValue() + const user = await this.userRepository.findOneByUuid(userUuid.value) + if (user === null) { + return Result.fail('Could not list authenticators: user not found.') + } + + const userIsEntitledToU2F = await this.featureService.userIsEntitledToFeature( + user, + FeatureIdentifier.UniversalSecondFactor, + ) + + if (!userIsEntitledToU2F) { + return Result.fail('Could not list authenticators: user is not entitled to U2F.') + } + const authenticators = await this.authenticatorRepository.findByUserUuid(userUuid) return Result.ok(authenticators) diff --git a/packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.spec.ts b/packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.spec.ts index 944243de8..57b4b09ae 100644 --- a/packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.spec.ts +++ b/packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.spec.ts @@ -7,11 +7,16 @@ import { Authenticator } from '../../Authenticator/Authenticator' import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge' import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface' import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' +import { FeatureServiceInterface } from '../../Feature/FeatureServiceInterface' +import { User } from '../../User/User' +import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { VerifyAuthenticatorRegistrationResponse } from './VerifyAuthenticatorRegistrationResponse' describe('VerifyAuthenticatorRegistrationResponse', () => { let authenticatorRepository: AuthenticatorRepositoryInterface let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface + let userRepository: UserRepositoryInterface + let featureService: FeatureServiceInterface const createUseCase = () => new VerifyAuthenticatorRegistrationResponse( @@ -20,6 +25,8 @@ describe('VerifyAuthenticatorRegistrationResponse', () => { 'standardnotes.com', ['localhost', 'https://app.standardnotes.com'], true, + userRepository, + featureService, ) beforeEach(() => { @@ -32,6 +39,12 @@ describe('VerifyAuthenticatorRegistrationResponse', () => { challenge: Buffer.from('challenge'), }, } as jest.Mocked) + + userRepository = {} as jest.Mocked + userRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked) + + featureService = {} as jest.Mocked + featureService.userIsEntitledToFeature = jest.fn().mockReturnValue(true) }) it('should return error if user uuid is invalid', async () => { @@ -57,6 +70,54 @@ describe('VerifyAuthenticatorRegistrationResponse', () => { ) }) + it('should return error if user is not entitled to feature', async () => { + featureService.userIsEntitledToFeature = jest.fn().mockReturnValue(false) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + attestationResponse: { + id: 'id', + rawId: 'rawId', + response: { + attestationObject: 'attestationObject', + clientDataJSON: 'clientDataJSON', + }, + type: 'public-key', + clientExtensionResults: {}, + } as jest.Mocked, + }) + + expect(result.isFailed()).toBeTruthy() + expect(result.getError()).toEqual( + 'Could not verify authenticator registration response: user is not entitled to U2F.', + ) + }) + + it('should return error if user is not found', async () => { + userRepository.findOneByUuid = jest.fn().mockReturnValue(null) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + attestationResponse: { + id: 'id', + rawId: 'rawId', + response: { + attestationObject: 'attestationObject', + clientDataJSON: 'clientDataJSON', + }, + type: 'public-key', + clientExtensionResults: {}, + } as jest.Mocked, + }) + + expect(result.isFailed()).toBeTruthy() + expect(result.getError()).toEqual('Could not verify authenticator registration response: user not found.') + }) + it('should return error if challenge is not found', async () => { authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue(null) diff --git a/packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.ts b/packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.ts index 94b4d2681..ea759f6c5 100644 --- a/packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.ts +++ b/packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.ts @@ -1,10 +1,13 @@ import { Dates, Result, UniqueEntityId, UseCaseInterface, Uuid } from '@standardnotes/domain-core' import { VerifiedRegistrationResponse, verifyRegistrationResponse } from '@simplewebauthn/server' +import { FeatureIdentifier } from '@standardnotes/features' import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface' import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface' import { Authenticator } from '../../Authenticator/Authenticator' import { VerifyAuthenticatorRegistrationResponseDTO } from './VerifyAuthenticatorRegistrationResponseDTO' +import { FeatureServiceInterface } from '../../Feature/FeatureServiceInterface' +import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface { constructor( @@ -13,6 +16,8 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface private relyingPartyId: string, private expectedOrigin: string[], private requireUserVerification: boolean, + private userRepository: UserRepositoryInterface, + private featureService: FeatureServiceInterface, ) {} async execute(dto: VerifyAuthenticatorRegistrationResponseDTO): Promise> { @@ -22,6 +27,20 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface } const userUuid = userUuidOrError.getValue() + const user = await this.userRepository.findOneByUuid(userUuid.value) + if (user === null) { + return Result.fail('Could not verify authenticator registration response: user not found.') + } + + const userIsEntitledToU2F = await this.featureService.userIsEntitledToFeature( + user, + FeatureIdentifier.UniversalSecondFactor, + ) + + if (!userIsEntitledToU2F) { + return Result.fail('Could not verify authenticator registration response: user is not entitled to U2F.') + } + const authenticatorChallenge = await this.authenticatorChallengeRepository.findByUserUuid(userUuid) if (!authenticatorChallenge) { return Result.fail('Could not verify authenticator registration response: challenge not found') diff --git a/yarn.lock b/yarn.lock index 0b1d5649d..b10cf7ff8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3299,7 +3299,7 @@ __metadata: "@standardnotes/domain-core": "workspace:^" "@standardnotes/domain-events": "workspace:*" "@standardnotes/domain-events-infra": "workspace:*" - "@standardnotes/features": "npm:^1.58.9" + "@standardnotes/features": "npm:^1.58.12" "@standardnotes/predicates": "workspace:*" "@standardnotes/responses": "npm:^1.13.9" "@standardnotes/security": "workspace:*" @@ -3482,15 +3482,15 @@ __metadata: languageName: node linkType: hard -"@standardnotes/features@npm:^1.58.9": - version: 1.58.9 - resolution: "@standardnotes/features@npm:1.58.9" +"@standardnotes/features@npm:^1.58.12": + version: 1.58.12 + resolution: "@standardnotes/features@npm:1.58.12" dependencies: "@standardnotes/common": "npm:^1.46.6" "@standardnotes/domain-core": "npm:^1.11.3" "@standardnotes/security": "npm:^1.7.6" reflect-metadata: "npm:^0.1.13" - checksum: 218350ee55d2f920e26c4041e1e307655cf9e755b83c7fd2165be2222d95b40154c0d325a362cc84ce960ccf8c07c6d95c6a8558ddabf6ee335462cf6bd22508 + checksum: 3fcd9a948848cf6fe567390a7740222fd96d10b8a9bceeaf608befcd7e24ac7374e1c87ed51c12ab62a9ed6036b3c6da82c78ab58f6b0c3f0c3c9aaa2b7ffdfe languageName: node linkType: hard