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:
Karol Sójko
2022-12-19 08:28:10 +01:00
committed by GitHub
parent abdaec89b7
commit bb4f1ff099
37 changed files with 467 additions and 1430 deletions

View File

@@ -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: {

View File

@@ -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,
)
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -1,5 +1 @@
export * from './Generator'
export * from './JwtSession'
export * from './Session'
export * from './TokenSession'
export * from './Types'