fix(snjs): refreshing sessions (#2106)
* fix(snjs): refreshing sessions * fix(snjs): bring back all tests * fix(snjs): passing session tokens values * fix(api): remove redundant specs * fix(snjs): add projecting sessions to storage values * fix(snjs): deps tree * fix(snjs): bring back subscription tests * fix(snjs): remove only tag for migration tests * fix(snjs): session specs
This commit is contained in:
@@ -82,6 +82,9 @@ import { SNLog } from '../Log'
|
||||
import { ChallengeResponse, ListedClientInterface } from '../Services'
|
||||
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
||||
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
||||
import { LegacySession, MapperInterface, Session } from '@standardnotes/domain-core'
|
||||
import { SessionStorageMapper } from '@Lib/Services/Mapping/SessionStorageMapper'
|
||||
import { LegacySessionStorageMapper } from '@Lib/Services/Mapping/LegacySessionStorageMapper'
|
||||
|
||||
/** How often to automatically sync, in milliseconds */
|
||||
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
|
||||
@@ -154,6 +157,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private integrityService!: ExternalServices.IntegrityService
|
||||
private statusService!: ExternalServices.StatusService
|
||||
private filesBackupService?: FilesBackupService
|
||||
private declare sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>
|
||||
private declare legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>
|
||||
|
||||
private internalEventBus!: ExternalServices.InternalEventBusInterface
|
||||
|
||||
@@ -1078,6 +1083,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
}
|
||||
|
||||
private constructServices() {
|
||||
this.createMappers()
|
||||
this.createPayloadManager()
|
||||
this.createItemManager()
|
||||
this.createDiskStorageManager()
|
||||
@@ -1169,6 +1175,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
;(this.mutatorService as unknown) = undefined
|
||||
;(this.filesBackupService as unknown) = undefined
|
||||
;(this.statusService as unknown) = undefined
|
||||
;(this.sessionStorageMapper as unknown) = undefined
|
||||
;(this.legacySessionStorageMapper as unknown) = undefined
|
||||
|
||||
this.services = []
|
||||
}
|
||||
@@ -1289,6 +1297,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
environment: this.environment,
|
||||
identifier: this.identifier,
|
||||
internalEventBus: this.internalEventBus,
|
||||
legacySessionStorageMapper: this.legacySessionStorageMapper,
|
||||
})
|
||||
this.services.push(this.migrationService)
|
||||
}
|
||||
@@ -1335,6 +1344,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.options.defaultHost,
|
||||
this.inMemoryStore,
|
||||
this.options.crypto,
|
||||
this.sessionStorageMapper,
|
||||
this.legacySessionStorageMapper,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.services.push(this.apiService)
|
||||
@@ -1419,9 +1430,15 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.options.appVersion,
|
||||
SnjsVersion,
|
||||
this.apiService.processMetaObject.bind(this.apiService),
|
||||
this.apiService.setSession.bind(this.apiService),
|
||||
)
|
||||
}
|
||||
|
||||
private createMappers() {
|
||||
this.sessionStorageMapper = new SessionStorageMapper()
|
||||
this.legacySessionStorageMapper = new LegacySessionStorageMapper()
|
||||
}
|
||||
|
||||
private createPayloadManager() {
|
||||
this.payloadManager = new InternalServices.PayloadManager(this.internalEventBus)
|
||||
this.services.push(this.payloadManager)
|
||||
@@ -1497,6 +1514,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.challengeService,
|
||||
this.webSocketsService,
|
||||
this.httpService,
|
||||
this.sessionStorageMapper,
|
||||
this.legacySessionStorageMapper,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.serviceObservers.push(
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SNSessionManager } from '../Services/Session/SessionManager'
|
||||
import { ApplicationIdentifier } from '@standardnotes/common'
|
||||
import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
||||
import { ChallengeService, SNSingletonManager, SNFeaturesService, DiskStorageService } from '@Lib/Services'
|
||||
import { LegacySession, MapperInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export type MigrationServices = {
|
||||
protocolService: EncryptionService
|
||||
@@ -17,5 +18,6 @@ export type MigrationServices = {
|
||||
featuresService: SNFeaturesService
|
||||
environment: Environment
|
||||
identifier: ApplicationIdentifier
|
||||
legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>
|
||||
internalEventBus: InternalEventBusInterface
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AnyKeyParamsContent, ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { JwtSession } from '../../Services/Session/Sessions/JwtSession'
|
||||
import { Migration } from '@Lib/Migrations/Migration'
|
||||
import { MigrationServices } from '../MigrationServices'
|
||||
import { PreviousSnjsVersion2_0_0 } from '../../Version'
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
PayloadTimestampDefaults,
|
||||
} from '@standardnotes/models'
|
||||
import { isMobileDevice } from '@standardnotes/services'
|
||||
import { LegacySession } from '@standardnotes/domain-core'
|
||||
|
||||
interface LegacyStorageContent extends Models.ItemContent {
|
||||
storage: unknown
|
||||
@@ -673,8 +673,13 @@ export class Migration2_0_0 extends Migration {
|
||||
}
|
||||
}
|
||||
|
||||
const session = new JwtSession(currentToken)
|
||||
this.services.storageService.setValue(Services.StorageKey.Session, session)
|
||||
const sessionOrError = LegacySession.create(currentToken)
|
||||
if (!sessionOrError.isFailed()) {
|
||||
this.services.storageService.setValue(
|
||||
Services.StorageKey.Session,
|
||||
this.services.legacySessionStorageMapper.toProjection(sessionOrError.getValue()),
|
||||
)
|
||||
}
|
||||
|
||||
/** Server has to be migrated separately on mobile */
|
||||
if (isEnvironmentMobile(this.services.environment)) {
|
||||
|
||||
@@ -34,23 +34,22 @@ import {
|
||||
API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS,
|
||||
} from '@standardnotes/services'
|
||||
import { FilesApiInterface } from '@standardnotes/files'
|
||||
|
||||
import { ServerSyncPushContextualPayload, SNFeatureRepo, FileContent } from '@standardnotes/models'
|
||||
import * as Responses from '@standardnotes/responses'
|
||||
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import { HttpResponseMeta } from '@standardnotes/api'
|
||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { ApiEndpointParam, ClientDisplayableError, CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
|
||||
import { HttpParams, HttpRequest, HttpVerb, SNHttpService } from './HttpService'
|
||||
import { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
||||
import { Paths } from './Paths'
|
||||
import { Session } from '../Session/Sessions/Session'
|
||||
import { TokenSession } from '../Session/Sessions/TokenSession'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { HttpResponseMeta } from '@standardnotes/api'
|
||||
import { UuidString } from '../../Types/UuidString'
|
||||
import merge from 'lodash/merge'
|
||||
import { SettingsServerInterface } from '../Settings/SettingsServerInterface'
|
||||
import { Strings } from '@Lib/Strings'
|
||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { ApiEndpointParam, ClientDisplayableError, CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
|
||||
/** Legacy api version field to be specified in params when calling v0 APIs. */
|
||||
const V0_API_VERSION = '20200115'
|
||||
@@ -66,7 +65,7 @@ export class SNApiService
|
||||
ItemsServerInterface,
|
||||
SettingsServerInterface
|
||||
{
|
||||
private session?: Session
|
||||
private session: Session | LegacySession | null
|
||||
public user?: Responses.User
|
||||
private registering = false
|
||||
private authenticating = false
|
||||
@@ -81,16 +80,20 @@ export class SNApiService
|
||||
private host: string,
|
||||
private inMemoryStore: KeyValueStoreInterface<string>,
|
||||
private crypto: PureCryptoInterface,
|
||||
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
|
||||
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
|
||||
this.session = null
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
;(this.httpService as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
this.invalidSessionObserver = undefined
|
||||
this.session = undefined
|
||||
this.session = null
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
@@ -145,14 +148,21 @@ export class SNApiService
|
||||
return this.filesHost
|
||||
}
|
||||
|
||||
public setSession(session: Session, persist = true): void {
|
||||
public setSession(session: Session | LegacySession, persist = true): void {
|
||||
this.session = session
|
||||
if (persist) {
|
||||
this.storageService.setValue(StorageKey.Session, session)
|
||||
let sessionProjection: Record<string, unknown>
|
||||
if (session instanceof Session) {
|
||||
sessionProjection = this.sessionStorageMapper.toProjection(session)
|
||||
} else {
|
||||
sessionProjection = this.legacySessionStorageMapper.toProjection(session)
|
||||
}
|
||||
|
||||
this.storageService.setValue(StorageKey.Session, sessionProjection)
|
||||
}
|
||||
}
|
||||
|
||||
public getSession(): Session | undefined {
|
||||
public getSession(): Session | LegacySession | null {
|
||||
return this.session
|
||||
}
|
||||
|
||||
@@ -252,7 +262,7 @@ export class SNApiService
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||
params,
|
||||
/** A session is optional here, if valid, endpoint bypasses 2FA and returns additional params */
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -289,7 +299,7 @@ export class SNApiService
|
||||
|
||||
signOut(): Promise<Responses.SignOutResponse> {
|
||||
const url = joinPaths(this.host, Paths.v1.signOut)
|
||||
return this.httpService.postAbsolute(url, undefined, this.session?.authorizationValue).catch((errorResponse) => {
|
||||
return this.httpService.postAbsolute(url, undefined, this.getSessionAccessToken()).catch((errorResponse) => {
|
||||
return errorResponse
|
||||
}) as Promise<Responses.SignOutResponse>
|
||||
}
|
||||
@@ -317,7 +327,7 @@ export class SNApiService
|
||||
...parameters.newKeyParams.getPortableValue(),
|
||||
})
|
||||
const response = await this.httpService
|
||||
.putAbsolute(url, params, this.session?.authorizationValue)
|
||||
.putAbsolute(url, params, this.getSessionAccessToken())
|
||||
.catch(async (errorResponse) => {
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
return this.refreshSessionThenRetryRequest({
|
||||
@@ -353,7 +363,7 @@ export class SNApiService
|
||||
[ApiEndpointParam.SyncDlLimit]: limit,
|
||||
})
|
||||
const response = await this.httpService
|
||||
.postAbsolute(url, params, this.session?.authorizationValue)
|
||||
.postAbsolute(url, params, this.getSessionAccessToken())
|
||||
.catch<Responses.HttpResponse>(async (errorResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -378,7 +388,7 @@ export class SNApiService
|
||||
return this.httpService
|
||||
.runHttp({
|
||||
...httpRequest,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
.catch((errorResponse) => {
|
||||
return errorResponse
|
||||
@@ -393,16 +403,54 @@ export class SNApiService
|
||||
}
|
||||
this.refreshingSession = true
|
||||
const url = joinPaths(this.host, Paths.v1.refreshSession)
|
||||
const session = this.session as TokenSession
|
||||
const session = this.session as Session
|
||||
const params = this.params({
|
||||
access_token: session.accessToken,
|
||||
refresh_token: session.refreshToken,
|
||||
access_token: session.accessToken.value,
|
||||
refresh_token: session.refreshToken.value,
|
||||
})
|
||||
const result = await this.httpService
|
||||
.postAbsolute(url, params)
|
||||
.then(async (response) => {
|
||||
const session = TokenSession.FromApiResponse(response as Responses.SessionRenewalResponse)
|
||||
await this.setSession(session)
|
||||
const sessionRenewalResponse = response as Responses.SessionRenewalResponse
|
||||
if (
|
||||
sessionRenewalResponse.error ||
|
||||
sessionRenewalResponse.data?.error ||
|
||||
!sessionRenewalResponse.data.session
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const accessTokenOrError = SessionToken.create(
|
||||
sessionRenewalResponse.data.session.access_token,
|
||||
sessionRenewalResponse.data.session.access_expiration,
|
||||
)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
|
||||
const refreshTokenOrError = SessionToken.create(
|
||||
sessionRenewalResponse.data.session.refresh_token,
|
||||
sessionRenewalResponse.data.session.refresh_expiration,
|
||||
)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const sessionOrError = Session.create(
|
||||
accessToken,
|
||||
refreshToken,
|
||||
sessionRenewalResponse.data.session.readonly_access,
|
||||
)
|
||||
if (sessionOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const session = sessionOrError.getValue()
|
||||
|
||||
this.session = session
|
||||
|
||||
this.setSession(session)
|
||||
this.processResponse(response)
|
||||
return response
|
||||
})
|
||||
@@ -411,6 +459,11 @@ export class SNApiService
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL)
|
||||
})
|
||||
this.refreshingSession = false
|
||||
|
||||
if (result === null) {
|
||||
return this.createErrorResponse(API_MESSAGE_INVALID_SESSION)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -421,7 +474,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, Paths.v1.sessions)
|
||||
const response = await this.httpService
|
||||
.getAbsolute(url, {}, this.session?.authorizationValue)
|
||||
.getAbsolute(url, {}, this.getSessionAccessToken())
|
||||
.catch(async (errorResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -444,7 +497,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, <string>Paths.v1.session(sessionId))
|
||||
const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService
|
||||
.deleteAbsolute(url, { uuid: sessionId }, this.session?.authorizationValue)
|
||||
.deleteAbsolute(url, { uuid: sessionId }, this.getSessionAccessToken())
|
||||
.catch((error: Responses.HttpResponse) => {
|
||||
const errorResponse = error as Responses.HttpResponse
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
@@ -467,7 +520,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, Paths.v1.itemRevisions(itemId))
|
||||
const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService
|
||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
||||
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||
.catch((errorResponse: Responses.HttpResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -492,7 +545,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, Paths.v1.itemRevision(itemId, entry.uuid))
|
||||
const response: Responses.SingleRevisionResponse | Responses.HttpResponse = await this.httpService
|
||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
||||
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||
.catch((errorResponse: Responses.HttpResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -510,7 +563,7 @@ export class SNApiService
|
||||
async getUserFeatures(userUuid: UuidString): Promise<Responses.HttpResponse | Responses.UserFeaturesResponse> {
|
||||
const url = joinPaths(this.host, Paths.v1.userFeatures(userUuid))
|
||||
const response = await this.httpService
|
||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
||||
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||
.catch((errorResponse: Responses.HttpResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -550,7 +603,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -568,7 +621,7 @@ export class SNApiService
|
||||
return this.tokenRefreshableRequest<Responses.UpdateSettingResponse>({
|
||||
verb: HttpVerb.Put,
|
||||
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
params,
|
||||
})
|
||||
@@ -578,7 +631,7 @@ export class SNApiService
|
||||
return await this.tokenRefreshableRequest<Responses.GetSettingResponse>({
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase() as SettingName)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
})
|
||||
}
|
||||
@@ -593,7 +646,7 @@ export class SNApiService
|
||||
this.host,
|
||||
Paths.v1.subscriptionSetting(userUuid, settingName.toLowerCase() as SubscriptionSettingName),
|
||||
),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
})
|
||||
}
|
||||
@@ -602,7 +655,7 @@ export class SNApiService
|
||||
return this.tokenRefreshableRequest<Responses.DeleteSettingResponse>({
|
||||
verb: HttpVerb.Delete,
|
||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
})
|
||||
}
|
||||
@@ -616,7 +669,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Delete,
|
||||
url,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_DELETE_REVISION,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
return response
|
||||
}
|
||||
@@ -635,7 +688,7 @@ export class SNApiService
|
||||
const response = await this.tokenRefreshableRequest({
|
||||
verb: HttpVerb.Get,
|
||||
url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
|
||||
})
|
||||
return response
|
||||
@@ -658,7 +711,7 @@ export class SNApiService
|
||||
const response: Responses.HttpResponse | Responses.PostSubscriptionTokensResponse = await this.request({
|
||||
verb: HttpVerb.Post,
|
||||
url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_ACCESS_PURCHASE,
|
||||
})
|
||||
return (response as Responses.PostSubscriptionTokensResponse).data?.token
|
||||
@@ -706,7 +759,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Post,
|
||||
url: joinPaths(this.host, Paths.v1.listedRegistration(this.user.uuid)),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_LISTED_REGISTRATION,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -725,7 +778,7 @@ export class SNApiService
|
||||
const response = await this.tokenRefreshableRequest<Responses.CreateValetTokenResponse>({
|
||||
verb: HttpVerb.Post,
|
||||
url: url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_CREATE_FILE_TOKEN,
|
||||
params,
|
||||
})
|
||||
@@ -860,7 +913,7 @@ export class SNApiService
|
||||
integrityPayloads,
|
||||
},
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INTEGRITY_CHECK_FAIL,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -869,7 +922,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.getSingleItem(itemUuid)),
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_SINGLE_ITEM_SYNC_FAIL,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -890,6 +943,18 @@ export class SNApiService
|
||||
}
|
||||
}
|
||||
|
||||
private getSessionAccessToken(): string | undefined {
|
||||
if (!this.session) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this.session instanceof Session) {
|
||||
return this.session.accessToken.value
|
||||
}
|
||||
|
||||
return this.session.accessToken
|
||||
}
|
||||
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return Promise.resolve({
|
||||
api: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './ApiService'
|
||||
export * from './HttpService'
|
||||
export * from './Paths'
|
||||
export * from '../Session/Sessions/Session'
|
||||
export * from '../Session/SessionManager'
|
||||
export * from './WebsocketsService'
|
||||
|
||||
@@ -290,7 +290,7 @@ export class ChallengeService extends AbstractService implements ChallengeServic
|
||||
if (operation.isFinished()) {
|
||||
this.deleteChallengeOperation(operation)
|
||||
|
||||
const observers = this.challengeObservers[challenge.id]
|
||||
const observers = this.challengeObservers[challenge.id] || []
|
||||
observers.forEach(clearChallengeObserver)
|
||||
observers.length = 0
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { LegacySession, MapperInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export class LegacySessionStorageMapper implements MapperInterface<LegacySession, Record<string, unknown>> {
|
||||
toDomain(projection: Record<string, unknown>): LegacySession {
|
||||
const { jwt } = projection
|
||||
|
||||
const legacySessionOrError = LegacySession.create(jwt as string)
|
||||
if (legacySessionOrError.isFailed()) {
|
||||
throw new Error(legacySessionOrError.getError())
|
||||
}
|
||||
|
||||
return legacySessionOrError.getValue()
|
||||
}
|
||||
|
||||
toProjection(domain: LegacySession): Record<string, unknown> {
|
||||
return {
|
||||
jwt: domain.accessToken,
|
||||
}
|
||||
}
|
||||
}
|
||||
40
packages/snjs/lib/Services/Mapping/SessionStorageMapper.ts
Normal file
40
packages/snjs/lib/Services/Mapping/SessionStorageMapper.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
|
||||
export class SessionStorageMapper implements MapperInterface<Session, Record<string, unknown>> {
|
||||
toDomain(projection: Record<string, unknown>): Session {
|
||||
const accessTokenOrError = SessionToken.create(
|
||||
projection.accessToken as string,
|
||||
projection.accessExpiration as number,
|
||||
)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
throw new Error(accessTokenOrError.getError())
|
||||
}
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
|
||||
const refreshTokenOrError = SessionToken.create(
|
||||
projection.refreshToken as string,
|
||||
projection.refreshExpiration as number,
|
||||
)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
throw new Error(refreshTokenOrError.getError())
|
||||
}
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const session = Session.create(accessToken, refreshToken, projection.readonlyAccess as boolean)
|
||||
if (session.isFailed()) {
|
||||
throw new Error(session.getError())
|
||||
}
|
||||
|
||||
return session.getValue()
|
||||
}
|
||||
|
||||
toProjection(domain: Session): Record<string, unknown> {
|
||||
return {
|
||||
accessToken: domain.accessToken.value,
|
||||
refreshToken: domain.refreshToken.value,
|
||||
accessExpiration: domain.accessToken.expiresAt,
|
||||
refreshExpiration: domain.refreshToken.expiresAt,
|
||||
readonlyAccess: domain.isReadOnly(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,21 +27,19 @@ import { Base64String } from '@standardnotes/sncrypto-common'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
|
||||
import { isNullOrUndefined } from '@standardnotes/utils'
|
||||
import { JwtSession } from './Sessions/JwtSession'
|
||||
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import { KeyParamsFromApiResponse, SNRootKeyParams, SNRootKey, CreateNewRootKey } from '@standardnotes/encryption'
|
||||
import * as Responses from '@standardnotes/responses'
|
||||
import { Subscription } from '@standardnotes/security'
|
||||
import * as Common from '@standardnotes/common'
|
||||
|
||||
import { RemoteSession, RawStorageValue } from './Sessions/Types'
|
||||
import { Session } from './Sessions/Session'
|
||||
import { SessionFromRawStorageValue } from './Sessions/Generator'
|
||||
import { ShareToken } from './ShareToken'
|
||||
import { SNApiService } from '../Api/ApiService'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { SNWebSocketsService } from '../Api/WebsocketsService'
|
||||
import { Strings } from '@Lib/Strings'
|
||||
import { Subscription } from '@standardnotes/security'
|
||||
import { TokenSession } from './Sessions/TokenSession'
|
||||
import { UuidString } from '@Lib/Types/UuidString'
|
||||
import * as Common from '@standardnotes/common'
|
||||
import * as Responses from '@standardnotes/responses'
|
||||
import { ChallengeService } from '../Challenge'
|
||||
import {
|
||||
ApiCallError,
|
||||
@@ -82,6 +80,8 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
private challengeService: ChallengeService,
|
||||
private webSocketsService: SNWebSocketsService,
|
||||
private httpService: HttpServiceInterface,
|
||||
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
|
||||
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -122,13 +122,23 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
|
||||
const rawSession = this.diskStorageService.getValue<RawStorageValue>(StorageKey.Session)
|
||||
if (rawSession) {
|
||||
const session = SessionFromRawStorageValue(rawSession)
|
||||
this.setSession(session, false)
|
||||
try {
|
||||
const session =
|
||||
'jwt' in rawSession
|
||||
? this.legacySessionStorageMapper.toDomain(rawSession)
|
||||
: this.sessionStorageMapper.toDomain(rawSession)
|
||||
|
||||
this.setSession(session, false)
|
||||
} catch (error) {
|
||||
console.error(`Could not deserialize session from storage: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setSession(session: Session, persist = true): void {
|
||||
this.httpService.setAuthorizationToken(session.authorizationValue)
|
||||
private setSession(session: Session | LegacySession, persist = true): void {
|
||||
if (session instanceof Session) {
|
||||
this.httpService.setSession(session)
|
||||
}
|
||||
|
||||
this.apiService.setSession(session, persist)
|
||||
|
||||
@@ -158,7 +168,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
public async signOut() {
|
||||
this.setUser(undefined)
|
||||
const session = this.apiService.getSession()
|
||||
if (session && session.canExpire()) {
|
||||
if (session && session instanceof Session) {
|
||||
await this.apiService.signOut()
|
||||
this.webSocketsService.closeWebSocketConnection()
|
||||
}
|
||||
@@ -560,17 +570,17 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
if (!session) {
|
||||
return new ClientDisplayableError('Cannot generate share token without active session')
|
||||
}
|
||||
if (!(session instanceof TokenSession)) {
|
||||
if (!(session instanceof Session)) {
|
||||
return new ClientDisplayableError('Cannot generate share token with non-token session')
|
||||
}
|
||||
|
||||
const keyParams = (await this.protocolService.getRootKeyParams()) as SNRootKeyParams
|
||||
|
||||
const payload: ShareToken = {
|
||||
accessToken: session.accessToken,
|
||||
refreshToken: session.refreshToken,
|
||||
accessExpiration: session.accessExpiration,
|
||||
refreshExpiration: session.refreshExpiration,
|
||||
accessToken: session.accessToken.value,
|
||||
refreshToken: session.refreshToken.value,
|
||||
accessExpiration: session.accessToken.expiresAt,
|
||||
refreshExpiration: session.refreshToken.expiresAt,
|
||||
readonlyAccess: true,
|
||||
masterKey: this.protocolService.getRootKey()?.masterKey as string,
|
||||
keyParams: keyParams.content,
|
||||
@@ -597,7 +607,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
|
||||
const user = sharePayload.user
|
||||
|
||||
const session = new TokenSession(
|
||||
const session = this.createSession(
|
||||
sharePayload.accessToken,
|
||||
sharePayload.accessExpiration,
|
||||
sharePayload.refreshToken,
|
||||
@@ -605,13 +615,15 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
sharePayload.readonlyAccess,
|
||||
)
|
||||
|
||||
await this.populateSession(rootKey, user, session, sharePayload.host)
|
||||
if (session !== null) {
|
||||
await this.populateSession(rootKey, user, session, sharePayload.host)
|
||||
}
|
||||
}
|
||||
|
||||
private async populateSession(
|
||||
rootKey: SNRootKey,
|
||||
user: Responses.User,
|
||||
session: Session,
|
||||
session: Session | LegacySession,
|
||||
host: string,
|
||||
wrappingKey?: SNRootKey,
|
||||
) {
|
||||
@@ -629,14 +641,17 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
}
|
||||
|
||||
private async handleAuthResponse(body: UserRegistrationResponseBody, rootKey: SNRootKey, wrappingKey?: SNRootKey) {
|
||||
const session = new TokenSession(
|
||||
const session = this.createSession(
|
||||
body.session.access_token,
|
||||
body.session.access_expiration,
|
||||
body.session.refresh_token,
|
||||
body.session.refresh_expiration,
|
||||
body.session.readonly_access,
|
||||
)
|
||||
await this.populateSession(rootKey, body.user, session, this.apiService.getHost(), wrappingKey)
|
||||
|
||||
if (session !== null) {
|
||||
await this.populateSession(rootKey, body.user, session, this.apiService.getHost(), wrappingKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -652,14 +667,51 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
|
||||
const isLegacyJwtResponse = data.token != undefined
|
||||
if (isLegacyJwtResponse) {
|
||||
const session = new JwtSession(data.token as string)
|
||||
await this.populateSession(rootKey, user, session, this.apiService.getHost(), wrappingKey)
|
||||
const sessionOrError = LegacySession.create(data.token as string)
|
||||
if (!sessionOrError.isFailed()) {
|
||||
await this.populateSession(rootKey, user, sessionOrError.getValue(), this.apiService.getHost(), wrappingKey)
|
||||
}
|
||||
} else if (data.session) {
|
||||
const session = TokenSession.FromApiResponse(response)
|
||||
await this.populateSession(rootKey, user, session, this.apiService.getHost(), wrappingKey)
|
||||
const session = this.createSession(
|
||||
data.session.access_token,
|
||||
data.session.access_expiration,
|
||||
data.session.refresh_token,
|
||||
data.session.refresh_expiration,
|
||||
data.session.readonly_access,
|
||||
)
|
||||
if (session !== null) {
|
||||
await this.populateSession(rootKey, user, session, this.apiService.getHost(), wrappingKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createSession(
|
||||
accessTokenValue: string,
|
||||
accessExpiration: number,
|
||||
refreshTokenValue: string,
|
||||
refreshExpiration: number,
|
||||
readonlyAccess: boolean,
|
||||
): Session | null {
|
||||
const accessTokenOrError = SessionToken.create(accessTokenValue, accessExpiration)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
|
||||
const refreshTokenOrError = SessionToken.create(refreshTokenValue, refreshExpiration)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const sessionOrError = Session.create(accessToken, refreshToken, readonlyAccess)
|
||||
if (sessionOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return sessionOrError.getValue()
|
||||
}
|
||||
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return Promise.resolve({
|
||||
session: {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { JwtSession } from './JwtSession'
|
||||
import { TokenSession } from './TokenSession'
|
||||
import { RawSessionPayload, RawStorageValue } from './Types'
|
||||
|
||||
export function SessionFromRawStorageValue(raw: RawStorageValue): JwtSession | TokenSession {
|
||||
if ('jwt' in raw) {
|
||||
return new JwtSession(raw.jwt as string)
|
||||
} else {
|
||||
const rawSession = raw as RawSessionPayload
|
||||
return new TokenSession(
|
||||
rawSession.accessToken,
|
||||
rawSession.accessExpiration,
|
||||
rawSession.refreshToken,
|
||||
rawSession.refreshExpiration,
|
||||
rawSession.readonlyAccess,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Session } from './Session'
|
||||
|
||||
/** Legacy, for protocol versions <= 003 */
|
||||
|
||||
export class JwtSession extends Session {
|
||||
public jwt: string
|
||||
|
||||
constructor(jwt: string) {
|
||||
super()
|
||||
this.jwt = jwt
|
||||
}
|
||||
|
||||
public get authorizationValue(): string {
|
||||
return this.jwt
|
||||
}
|
||||
|
||||
public canExpire(): false {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export abstract class Session {
|
||||
public abstract canExpire(): boolean
|
||||
|
||||
/** Return the token that should be included in the header of authorized network requests */
|
||||
public abstract get authorizationValue(): string
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { SessionBody, SessionRenewalResponse } from '@standardnotes/responses'
|
||||
import { Session } from './Session'
|
||||
|
||||
/** For protocol versions >= 004 */
|
||||
export class TokenSession extends Session {
|
||||
static FromApiResponse(response: SessionRenewalResponse) {
|
||||
const body = response.data.session as SessionBody
|
||||
const accessToken: string = body.access_token
|
||||
const refreshToken: string = body.refresh_token
|
||||
const accessExpiration: number = body.access_expiration
|
||||
const refreshExpiration: number = body.refresh_expiration
|
||||
const readonlyAccess: boolean = body.readonly_access
|
||||
|
||||
return new TokenSession(accessToken, accessExpiration, refreshToken, refreshExpiration, readonlyAccess)
|
||||
}
|
||||
|
||||
constructor(
|
||||
public accessToken: string,
|
||||
public accessExpiration: number,
|
||||
public refreshToken: string,
|
||||
public refreshExpiration: number,
|
||||
private readonlyAccess: boolean,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
isReadOnly() {
|
||||
return this.readonlyAccess
|
||||
}
|
||||
|
||||
private getExpireAt() {
|
||||
return this.accessExpiration || 0
|
||||
}
|
||||
|
||||
public get authorizationValue() {
|
||||
return this.accessToken
|
||||
}
|
||||
|
||||
public canExpire() {
|
||||
return true
|
||||
}
|
||||
|
||||
public isExpired() {
|
||||
return this.getExpireAt() < Date.now()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
export * from './Generator'
|
||||
export * from './JwtSession'
|
||||
export * from './Session'
|
||||
export * from './TokenSession'
|
||||
export * from './Types'
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('server session', function () {
|
||||
|
||||
async function sleepUntilSessionExpires(application, basedOnAccessToken = true) {
|
||||
const currentSession = application.apiService.session
|
||||
const timestamp = basedOnAccessToken ? currentSession.accessExpiration : currentSession.refreshExpiration
|
||||
const timestamp = basedOnAccessToken ? currentSession.accessToken.expiresAt : currentSession.refreshToken.expiresAt
|
||||
const timeRemaining = (timestamp - Date.now()) / 1000 // in ms
|
||||
/*
|
||||
If the token has not expired yet, we will return the remaining time.
|
||||
@@ -98,12 +98,12 @@ describe('server session', function () {
|
||||
// After the above sync request is completed, we obtain the session information.
|
||||
const sessionAfterSync = this.application.apiService.getSession()
|
||||
|
||||
expect(sessionBeforeSync).to.not.equal(sessionAfterSync)
|
||||
expect(sessionBeforeSync.accessToken).to.not.equal(sessionAfterSync.accessToken)
|
||||
expect(sessionBeforeSync.refreshToken).to.not.equal(sessionAfterSync.refreshToken)
|
||||
expect(sessionBeforeSync.accessExpiration).to.be.lessThan(sessionAfterSync.accessExpiration)
|
||||
expect(sessionBeforeSync.equals(sessionAfterSync)).to.not.equal(true)
|
||||
expect(sessionBeforeSync.accessToken.value).to.not.equal(sessionAfterSync.accessToken.value)
|
||||
expect(sessionBeforeSync.refreshToken.value).to.not.equal(sessionAfterSync.refreshToken.value)
|
||||
expect(sessionBeforeSync.accessToken.expiresAt).to.be.lessThan(sessionAfterSync.accessToken.expiresAt)
|
||||
// New token should expire in the future.
|
||||
expect(sessionAfterSync.accessExpiration).to.be.greaterThan(Date.now())
|
||||
expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now())
|
||||
})
|
||||
|
||||
it('should succeed when a sync request is perfomed after signing into an ephemeral session', async function () {
|
||||
@@ -142,14 +142,22 @@ describe('server session', function () {
|
||||
const sessionFromStorage = await getSessionFromStorage(this.application)
|
||||
const sessionFromApiService = this.application.apiService.getSession()
|
||||
|
||||
expect(sessionFromStorage).to.equal(sessionFromApiService)
|
||||
expect(sessionFromStorage.accessToken).to.equal(sessionFromApiService.accessToken.value)
|
||||
expect(sessionFromStorage.refreshToken).to.equal(sessionFromApiService.refreshToken.value)
|
||||
expect(sessionFromStorage.accessExpiration).to.equal(sessionFromApiService.accessToken.expiresAt)
|
||||
expect(sessionFromStorage.refreshExpiration).to.equal(sessionFromApiService.refreshToken.expiresAt)
|
||||
expect(sessionFromStorage.readonlyAccess).to.equal(sessionFromApiService.isReadOnly())
|
||||
|
||||
await this.application.apiService.refreshSession()
|
||||
|
||||
const updatedSessionFromStorage = await getSessionFromStorage(this.application)
|
||||
const updatedSessionFromApiService = this.application.apiService.getSession()
|
||||
|
||||
expect(updatedSessionFromStorage).to.equal(updatedSessionFromApiService)
|
||||
expect(updatedSessionFromStorage.accessToken).to.equal(updatedSessionFromApiService.accessToken.value)
|
||||
expect(updatedSessionFromStorage.refreshToken).to.equal(updatedSessionFromApiService.refreshToken.value)
|
||||
expect(updatedSessionFromStorage.accessExpiration).to.equal(updatedSessionFromApiService.accessToken.expiresAt)
|
||||
expect(updatedSessionFromStorage.refreshExpiration).to.equal(updatedSessionFromApiService.refreshToken.expiresAt)
|
||||
expect(updatedSessionFromStorage.readonlyAccess).to.equal(updatedSessionFromApiService.isReadOnly())
|
||||
})
|
||||
|
||||
it('should be performed successfully and terminate session with a valid access token', async function () {
|
||||
@@ -221,8 +229,16 @@ describe('server session', function () {
|
||||
let { application, password } = await Factory.createAndInitSimpleAppContext({
|
||||
registerUser: true,
|
||||
})
|
||||
const fakeSession = application.apiService.getSession()
|
||||
fakeSession.accessToken = 'this-is-a-fake-token-1234'
|
||||
|
||||
application.diskStorageService.setValue(StorageKey.Session, {
|
||||
accessToken: 'this-is-a-fake-token-1234',
|
||||
refreshToken: 'this-is-a-fake-token-1234',
|
||||
accessExpiration: 999999999999999,
|
||||
refreshExpiration: 99999999999999,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
application.sessions.initializeFromDisk()
|
||||
|
||||
Factory.ignoreChallenges(application)
|
||||
|
||||
const newEmail = UuidGenerator.GenerateUuid()
|
||||
@@ -311,8 +327,15 @@ describe('server session', function () {
|
||||
password: this.password,
|
||||
})
|
||||
|
||||
const fakeSession = this.application.apiService.getSession()
|
||||
fakeSession.accessToken = 'this-is-a-fake-token-1234'
|
||||
this.application.diskStorageService.setValue(StorageKey.Session, {
|
||||
accessToken: 'this-is-a-fake-token-1234',
|
||||
refreshToken: 'this-is-a-fake-token-1234',
|
||||
accessExpiration: 999999999999999,
|
||||
refreshExpiration: 99999999999999,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
this.application.sessions.initializeFromDisk()
|
||||
|
||||
Factory.ignoreChallenges(this.application)
|
||||
const changePasswordResponse = await this.application.changePassword(this.password, this.newPassword)
|
||||
expect(changePasswordResponse.error.message).to.equal('Invalid login credentials.')
|
||||
@@ -354,7 +377,7 @@ describe('server session', function () {
|
||||
expect(currentSession).to.be.ok
|
||||
expect(currentSession.accessToken).to.be.ok
|
||||
expect(currentSession.refreshToken).to.be.ok
|
||||
expect(currentSession.accessExpiration).to.be.greaterThan(Date.now())
|
||||
expect(currentSession.accessToken.expiresAt).to.be.greaterThan(Date.now())
|
||||
})
|
||||
|
||||
it('should fail when renewing a session with an expired refresh token', async function () {
|
||||
@@ -392,10 +415,16 @@ describe('server session', function () {
|
||||
password: this.password,
|
||||
})
|
||||
|
||||
const fakeSession = this.application.apiService.getSession()
|
||||
fakeSession.refreshToken = 'this-is-a-fake-token-1234'
|
||||
const originalSession = this.application.apiService.getSession()
|
||||
|
||||
await this.application.apiService.setSession(fakeSession, true)
|
||||
this.application.diskStorageService.setValue(StorageKey.Session, {
|
||||
accessToken: originalSession.accessToken.value,
|
||||
refreshToken: 'this-is-a-fake-token-1234',
|
||||
accessExpiration: originalSession.accessToken.expiresAt,
|
||||
refreshExpiration: originalSession.refreshToken.expiresAt,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
this.application.sessions.initializeFromDisk()
|
||||
|
||||
const refreshSessionResponse = await this.application.apiService.refreshSession()
|
||||
|
||||
@@ -530,8 +559,14 @@ describe('server session', function () {
|
||||
const oldRootKey = await appA.protocolService.getRootKey()
|
||||
|
||||
/** Set the session as nonsense */
|
||||
appA.apiService.session.accessToken = 'foo'
|
||||
appA.apiService.session.refreshToken = 'bar'
|
||||
appA.diskStorageService.setValue(StorageKey.Session, {
|
||||
accessToken: 'foo',
|
||||
refreshToken: 'bar',
|
||||
accessExpiration: 999999999999999,
|
||||
refreshExpiration: 999999999999999,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
appA.sessions.initializeFromDisk()
|
||||
|
||||
/** Perform an authenticated network request */
|
||||
await appA.sync.sync()
|
||||
@@ -540,8 +575,8 @@ describe('server session', function () {
|
||||
await Factory.sleep(5.0)
|
||||
|
||||
expect(didPromptForSignIn).to.equal(true)
|
||||
expect(appA.apiService.session.accessToken).to.not.equal('foo')
|
||||
expect(appA.apiService.session.refreshToken).to.not.equal('bar')
|
||||
expect(appA.apiService.session.accessToken.value).to.not.equal('foo')
|
||||
expect(appA.apiService.session.refreshToken.value).to.not.equal('bar')
|
||||
|
||||
/** Expect that the session recovery replaces the global root key */
|
||||
const newRootKey = await appA.protocolService.getRootKey()
|
||||
@@ -646,9 +681,14 @@ describe('server session', function () {
|
||||
password: this.password,
|
||||
})
|
||||
|
||||
const invalidSession = this.application.apiService.getSession()
|
||||
invalidSession.accessToken = undefined
|
||||
invalidSession.refreshToken = undefined
|
||||
this.application.diskStorageService.setValue(StorageKey.Session, {
|
||||
accessToken: undefined,
|
||||
refreshToken: undefined,
|
||||
accessExpiration: 999999999999999,
|
||||
refreshExpiration: 999999999999999,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
this.application.sessions.initializeFromDisk()
|
||||
|
||||
const storageKey = this.application.diskStorageService.getPersistenceKey()
|
||||
expect(localStorage.getItem(storageKey)).to.be.ok
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as Factory from './lib/factory.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe.skip('subscriptions', function () {
|
||||
describe('subscriptions', function () {
|
||||
this.timeout(Factory.TwentySecondTimeout)
|
||||
|
||||
let application
|
||||
|
||||
@@ -83,5 +83,8 @@
|
||||
"webpack": "*",
|
||||
"webpack-cli": "*",
|
||||
"webpack-merge": "^5.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/domain-core": "^1.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user