Compare commits

..

11 Commits

Author SHA1 Message Date
standardci
2daa145867 chore(release): publish new version
- @standardnotes/auth-server@1.93.5
2023-03-09 10:02:53 +00:00
Karol Sójko
4bd5fb22b4 fix(auth): remove migrate email settings procedure 2023-03-09 10:48:40 +01:00
standardci
78533a6045 chore(release): publish new version
- @standardnotes/auth-server@1.93.4
2023-03-09 06:25:32 +00:00
Karol Sójko
e1c533a15e fix(auth): change response from verifying authenticator registration 2023-03-09 07:09:43 +01:00
standardci
b6c2bb8023 chore(release): publish new version
- @standardnotes/auth-server@1.93.3
2023-03-09 05:59:55 +00:00
Karol Sójko
c45653a50a fix(auth): remove authenticator names from server 2023-03-09 06:46:35 +01:00
Karol Sójko
d827513b73 fix(auth): migrate encrypted sign in settings 2023-03-09 06:34:50 +01:00
standardci
ad183ca621 chore(release): publish new version
- @standardnotes/auth-server@1.93.2
2023-03-08 13:22:34 +00:00
Karol Sójko
1d11c5a186 fix(auth): authentication options 2023-03-08 14:08:40 +01:00
standardci
e84e78ec55 chore(release): publish new version
- @standardnotes/auth-server@1.93.1
2023-03-08 12:57:57 +00:00
Karol Sójko
f91e4316ff fix(auth): migrate muted email notifications settings 2023-03-08 13:42:45 +01:00
26 changed files with 68 additions and 221 deletions

View File

@@ -3,6 +3,37 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.93.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.4...@standardnotes/auth-server@1.93.5) (2023-03-09)
### Bug Fixes
* **auth:** remove migrate email settings procedure ([4bd5fb2](https://github.com/standardnotes/server/commit/4bd5fb22b447b0e0fdb136aa46ddc812c8b272cd))
## [1.93.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.3...@standardnotes/auth-server@1.93.4) (2023-03-09)
### Bug Fixes
* **auth:** change response from verifying authenticator registration ([e1c533a](https://github.com/standardnotes/server/commit/e1c533a15e33e215e90fbe15d2d4994605eaa1bd))
## [1.93.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.2...@standardnotes/auth-server@1.93.3) (2023-03-09)
### Bug Fixes
* **auth:** migrate encrypted sign in settings ([d827513](https://github.com/standardnotes/server/commit/d827513b73a57fbdb72c3112f32dc2a296103450))
* **auth:** remove authenticator names from server ([c45653a](https://github.com/standardnotes/server/commit/c45653a50a9d25de1e0fc86127ff6931dc98406d))
## [1.93.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.1...@standardnotes/auth-server@1.93.2) (2023-03-08)
### Bug Fixes
* **auth:** authentication options ([1d11c5a](https://github.com/standardnotes/server/commit/1d11c5a1865f81ca57d0ad4313cc3df497b4c445))
## [1.93.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.93.0...@standardnotes/auth-server@1.93.1) (2023-03-08)
### Bug Fixes
* **auth:** migrate muted email notifications settings ([f91e431](https://github.com/standardnotes/server/commit/f91e4316ff4993d032c016bb233b93a9f3356cf3))
# [1.93.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.92.0...@standardnotes/auth-server@1.93.0) (2023-03-08)
### Bug Fixes

View File

@@ -1,137 +0,0 @@
import 'reflect-metadata'
import 'newrelic'
import { Stream } from 'stream'
import { Logger } from 'winston'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
import { SettingName } from '@standardnotes/settings'
import { EmailLevel } from '@standardnotes/domain-core'
import { UserSubscriptionServiceInterface } from '../src/Domain/Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
import { SubscriptionSettingServiceInterface } from '../src/Domain/Setting/SubscriptionSettingServiceInterface'
import { EncryptionVersion } from '../src/Domain/Encryption/EncryptionVersion'
const requestSettingMigration = async (
settingRepository: SettingRepositoryInterface,
subscriptionSettingService: SubscriptionSettingServiceInterface,
userRepository: UserRepositoryInterface,
userSubscriptionService: UserSubscriptionServiceInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => {
const stream = await settingRepository.streamAllByNameAndValue(
SettingName.create(SettingName.NAMES.MuteSignInEmails).getValue(),
'not_muted',
)
return new Promise((resolve, reject) => {
stream
.pipe(
new Stream.Transform({
objectMode: true,
transform: async (setting, _encoding, callback) => {
const user = await userRepository.findOneByUuid(setting.setting_user_uuid)
if (!user) {
callback()
return
}
const { regularSubscription, sharedSubscription } =
await userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid)
const subscription = sharedSubscription ?? regularSubscription
if (!subscription) {
await domainEventPublisher.publish(
domainEventFactory.createMuteEmailsSettingChangedEvent({
username: user.email,
mute: true,
emailSubscriptionRejectionLevel: EmailLevel.LEVELS.SignIn,
}),
)
await settingRepository.deleteByUserUuid({
userUuid: user.uuid,
settingName: SettingName.NAMES.MuteSignInEmails,
})
callback()
return
}
await subscriptionSettingService.createOrReplace({
userSubscription: subscription,
props: {
name: SettingName.NAMES.MuteSignInEmails,
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
unencryptedValue: 'not_muted',
},
})
await settingRepository.deleteByUserUuid({
userUuid: user.uuid,
settingName: SettingName.NAMES.MuteSignInEmails,
})
callback()
},
}),
)
.on('finish', resolve)
.on('error', reject)
})
}
const container = new ContainerConfigLoader()
void container.load().then((container) => {
dayjs.extend(utc)
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Logger)
logger.info('Starting migration of mute sign in emails settings to subscription settings...')
const settingRepository: SettingRepositoryInterface = container.get(TYPES.SettingRepository)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
const subscriptionSettingService: SubscriptionSettingServiceInterface = container.get(
TYPES.SubscriptionSettingService,
)
const userRepository: UserRepositoryInterface = container.get(TYPES.UserRepository)
const userSubscriptionService: UserSubscriptionServiceInterface = container.get(TYPES.UserSubscriptionService)
Promise.resolve(
requestSettingMigration(
settingRepository,
subscriptionSettingService,
userRepository,
userSubscriptionService,
domainEventFactory,
domainEventPublisher,
),
)
.then(() => {
logger.info('Migration of mute sign in emails settings to subscription settings finished successfully.')
process.exit(0)
})
.catch((error) => {
logger.error(`Migration of mute sign in emails settings to subscription settings failed: ${error.message}`)
process.exit(1)
})
})

View File

@@ -1,11 +0,0 @@
'use strict'
const path = require('path')
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/migrate_email_settings.js')))
Object.defineProperty(exports, '__esModule', { value: true })
exports.default = index

View File

@@ -40,11 +40,6 @@ case "$COMMAND" in
node docker/entrypoint-user-email-backup.js $EMAIL
;;
'migrate-email-settings' )
echo "[Docker] Starting Email Settings Migration..."
node docker/entrypoint-migrate-email-settings.js
;;
'dropbox-daily-backup' )
echo "[Docker] Starting Dropbox Daily Backup..."
node docker/entrypoint-backup.js dropbox daily

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class removeAuthenticatorNamesFromServer1678340701766 implements MigrationInterface {
name = 'removeAuthenticatorNamesFromServer1678340701766'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `name`')
}
public async down(): Promise<void> {
return
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.93.0",
"version": "1.93.5",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -88,7 +88,6 @@ export class AuthenticatorsController {
): Promise<HttpResponse<VerifyAuthenticatorRegistrationResponseResponseBody>> {
const result = await this.verifyAuthenticatorRegistrationResponse.execute({
userUuid: params.userUuid,
name: params.name,
attestationResponse: params.attestationResponse,
})
@@ -105,7 +104,7 @@ export class AuthenticatorsController {
return {
status: HttpStatusCode.Success,
data: { success: result.getValue() },
data: { id: result.getValue().toString() },
}
}

View File

@@ -6,7 +6,6 @@ describe('Authenticator', () => {
it('should create an entity', () => {
const entityOrError = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),

View File

@@ -1,7 +1,6 @@
import { Dates, Uuid } from '@standardnotes/domain-core'
export interface AuthenticatorProps {
name: string
userUuid: Uuid
credentialId: Uint8Array
credentialPublicKey: Uint8Array

View File

@@ -11,6 +11,7 @@ export interface SettingRepositoryInterface {
findLastByNameAndUserUuid(name: string, userUuid: string): Promise<Setting | null>
findAllByUserUuid(userUuid: string): Promise<Setting[]>
streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream>
streamAllByName(name: SettingName): Promise<ReadStream>
deleteByUserUuid(dto: DeleteSettingDto): Promise<void>
save(setting: Setting): Promise<Setting>
}

View File

@@ -12,7 +12,6 @@ describe('DeleteAuthenticator', () => {
beforeEach(() => {
authenticator = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),

View File

@@ -24,7 +24,6 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
beforeEach(() => {
const authenticator = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),
@@ -54,7 +53,7 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not generate authenticator registration options: Username cannot be empty')
expect(result.getError()).toBe('Could not generate authenticator authentication options: Username cannot be empty')
})
it('should return error if user uuid is not valid', async () => {
@@ -70,7 +69,7 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe(
'Could not generate authenticator registration options: Given value is not a valid uuid: invalid',
'Could not generate authenticator authentication options: Given value is not a valid uuid: invalid',
)
})
@@ -97,7 +96,7 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not generate authenticator registration options: Oops')
expect(result.getError()).toBe('Could not generate authenticator authentication options: Oops')
mock.mockRestore()
})

View File

@@ -19,7 +19,7 @@ export class GenerateAuthenticatorAuthenticationOptions implements UseCaseInterf
async execute(dto: GenerateAuthenticatorAuthenticationOptionsDTO): Promise<Result<Record<string, unknown>>> {
const usernameOrError = Username.create(dto.username)
if (usernameOrError.isFailed()) {
return Result.fail(`Could not generate authenticator registration options: ${usernameOrError.getError()}`)
return Result.fail(`Could not generate authenticator authentication options: ${usernameOrError.getError()}`)
}
const username = usernameOrError.getValue()
@@ -46,7 +46,7 @@ export class GenerateAuthenticatorAuthenticationOptions implements UseCaseInterf
const userUuidOrError = Uuid.create(user.uuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not generate authenticator registration options: ${userUuidOrError.getError()}`)
return Result.fail(`Could not generate authenticator authentication options: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
@@ -67,7 +67,7 @@ export class GenerateAuthenticatorAuthenticationOptions implements UseCaseInterf
})
if (authenticatorChallengeOrError.isFailed()) {
return Result.fail(
`Could not generate authenticator registration options: ${authenticatorChallengeOrError.getError()}`,
`Could not generate authenticator authentication options: ${authenticatorChallengeOrError.getError()}`,
)
}
const authenticatorChallenge = authenticatorChallengeOrError.getValue()

View File

@@ -21,7 +21,6 @@ describe('GenerateAuthenticatorRegistrationOptions', () => {
beforeEach(() => {
const authenticator = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),

View File

@@ -24,7 +24,6 @@ describe('VerifyAuthenticatorAuthenticationResponse', () => {
beforeEach(() => {
const authenticator = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),

View File

@@ -38,7 +38,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: 'invalid',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -56,27 +55,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
)
})
it('should return error if name is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: '',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
response: {
attestationObject: Buffer.from('attestationObject'),
clientDataJSON: Buffer.from('clientDataJSON'),
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not verify authenticator registration response: Given value is empty: ')
})
it('should return error if challenge is not found', async () => {
authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue(null)
@@ -84,7 +62,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -125,7 +102,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -159,7 +135,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -195,7 +170,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -245,7 +219,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
@@ -289,7 +262,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
name: 'name',
attestationResponse: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),

View File

@@ -1,4 +1,4 @@
import { Dates, Result, UseCaseInterface, Uuid, Validator } from '@standardnotes/domain-core'
import { Dates, Result, UniqueEntityId, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { VerifiedRegistrationResponse, verifyRegistrationResponse } from '@simplewebauthn/server'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
@@ -6,7 +6,7 @@ import { AuthenticatorRepositoryInterface } from '../../Authenticator/Authentica
import { Authenticator } from '../../Authenticator/Authenticator'
import { VerifyAuthenticatorRegistrationResponseDTO } from './VerifyAuthenticatorRegistrationResponseDTO'
export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface<boolean> {
export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface<UniqueEntityId> {
constructor(
private authenticatorRepository: AuthenticatorRepositoryInterface,
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
@@ -15,18 +15,13 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface
private requireUserVerification: boolean,
) {}
async execute(dto: VerifyAuthenticatorRegistrationResponseDTO): Promise<Result<boolean>> {
async execute(dto: VerifyAuthenticatorRegistrationResponseDTO): Promise<Result<UniqueEntityId>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not verify authenticator registration response: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const nameValidation = Validator.isNotEmpty(dto.name)
if (nameValidation.isFailed()) {
return Result.fail(`Could not verify authenticator registration response: ${nameValidation.getError()}`)
}
const authenticatorChallenge = await this.authenticatorChallengeRepository.findByUserUuid(userUuid)
if (!authenticatorChallenge) {
return Result.fail('Could not verify authenticator registration response: challenge not found')
@@ -55,7 +50,6 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface
const authenticatorOrError = Authenticator.create({
userUuid,
name: dto.name,
counter: verification.registrationInfo.counter,
credentialBackedUp: verification.registrationInfo.credentialBackedUp,
credentialDeviceType: verification.registrationInfo.credentialDeviceType,
@@ -71,6 +65,6 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface
await this.authenticatorRepository.save(authenticator)
return Result.ok(true)
return Result.ok(authenticator.id)
}
}

View File

@@ -1,5 +1,4 @@
export interface VerifyAuthenticatorRegistrationResponseDTO {
userUuid: string
name: string
attestationResponse: Record<string, unknown>
}

View File

@@ -1,4 +1,3 @@
export interface AuthenticatorHttpProjection {
id: string
name: string
}

View File

@@ -1,5 +1,4 @@
export interface VerifyAuthenticatorRegistrationResponseRequestParams {
userUuid: string
name: string
attestationResponse: Record<string, unknown>
}

View File

@@ -1,3 +1,3 @@
export interface VerifyAuthenticatorRegistrationResponseResponseBody {
success: boolean
id: string
}

View File

@@ -52,7 +52,6 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
const result = await this.authenticatorsController.verifyRegistrationResponse({
userUuid: response.locals.user.uuid,
attestationResponse: request.body.attestationResponse,
name: request.body.name,
})
return this.json(result.data, result.status)

View File

@@ -29,6 +29,16 @@ export class MySQLSettingRepository implements SettingRepositoryInterface {
.getOne()
}
async streamAllByName(name: SettingName): Promise<ReadStream> {
return this.ormRepository
.createQueryBuilder('setting')
.where('setting.name = :name', {
name: name.value,
})
.orderBy('updated_at', 'ASC')
.stream()
}
async streamAllByNameAndValue(name: SettingName, value: string): Promise<ReadStream> {
return this.ormRepository
.createQueryBuilder('setting')

View File

@@ -11,13 +11,6 @@ export class TypeORMAuthenticator {
})
declare userUuid: string
@Column({
name: 'name',
type: 'varchar',
length: 255,
})
declare name: string
@Column({
name: 'credential_id',
type: 'text',

View File

@@ -11,7 +11,6 @@ export class AuthenticatorHttpMapper implements MapperInterface<Authenticator, A
toProjection(domain: Authenticator): AuthenticatorHttpProjection {
return {
id: domain.id.toString(),
name: domain.props.name,
}
}
}

View File

@@ -20,7 +20,6 @@ export class AuthenticatorPersistenceMapper implements MapperInterface<Authentic
const authenticatorOrError = Authenticator.create(
{
userUuid,
name: projection.name,
counter: projection.counter,
credentialBackedUp: projection.credentialBackedUp,
credentialDeviceType: projection.credentialDeviceType,
@@ -43,7 +42,6 @@ export class AuthenticatorPersistenceMapper implements MapperInterface<Authentic
const typeorm = new TypeORMAuthenticator()
typeorm.uuid = domain.id.toString()
typeorm.name = domain.props.name
typeorm.userUuid = domain.props.userUuid.value
typeorm.credentialId = Buffer.from(domain.props.credentialId).toString('base64url')
typeorm.credentialPublicKey = Buffer.from(domain.props.credentialPublicKey.buffer)