feat(auth): harden authentication with argon2 passwords and rotating hashed refresh tokens
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-20 - 1.17.0 - feat(auth)
|
||||||
|
harden authentication with argon2 passwords and rotating hashed refresh tokens
|
||||||
|
|
||||||
|
- replace SHA-256 password hashing with argon2 while preserving verification and upgrade support for legacy hashes
|
||||||
|
- rotate refresh tokens on JWT refresh, detect token reuse, and invalidate compromised sessions
|
||||||
|
- store refresh and transfer tokens as hashes with one-time transfer token validation and expiry
|
||||||
|
- persist refresh tokens separately on the client so sessions can recover and refresh without embedding tokens in JWTs
|
||||||
|
- add authentication tests covering password verification, legacy hash migration, refresh token rotation, reuse detection, and one-time transfer tokens
|
||||||
|
|
||||||
## 2026-01-29 - 1.16.0 - feat(dev)
|
## 2026-01-29 - 1.16.0 - feat(dev)
|
||||||
add local development docs, update tswatch preset and add Playwright screenshots
|
add local development docs, update tswatch preset and add Playwright screenshots
|
||||||
|
|
||||||
|
|||||||
+25
-23
@@ -6,7 +6,7 @@
|
|||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run build",
|
"test": "pnpm run build && tstest test/",
|
||||||
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle",
|
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle",
|
||||||
"watch": "tswatch",
|
"watch": "tswatch",
|
||||||
"start": "(node cli.js)",
|
"start": "(node cli.js)",
|
||||||
@@ -16,49 +16,51 @@
|
|||||||
"author": "Task Venture Capital GmbH",
|
"author": "Task Venture Capital GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.2.5",
|
"@api.global/typedrequest": "^3.3.0",
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@api.global/typedserver": "^8.3.0",
|
"@api.global/typedserver": "^8.4.6",
|
||||||
"@api.global/typedsocket": "^4.1.0",
|
"@api.global/typedsocket": "^4.1.2",
|
||||||
"@consent.software/catalog": "^2.0.1",
|
"@consent.software/catalog": "^2.0.1",
|
||||||
"@design.estate/dees-catalog": "^3.41.4",
|
"@design.estate/dees-catalog": "^3.81.0",
|
||||||
"@design.estate/dees-domtools": "^2.3.8",
|
"@design.estate/dees-domtools": "^2.5.4",
|
||||||
"@design.estate/dees-element": "^2.1.6",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@git.zone/tspublish": "^1.11.0",
|
"@git.zone/tspublish": "^1.11.5",
|
||||||
"@push.rocks/lik": "^6.2.2",
|
"@push.rocks/lik": "^6.4.0",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartcli": "^4.0.20",
|
"@push.rocks/smartcli": "^4.0.20",
|
||||||
"@push.rocks/smartdata": "^7.0.15",
|
"@push.rocks/smartdata": "^7.1.7",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartfile": "^13.1.0",
|
"@push.rocks/smartfile": "^13.1.0",
|
||||||
"@push.rocks/smarthash": "^3.2.6",
|
"@push.rocks/smarthash": "^3.2.6",
|
||||||
"@push.rocks/smartinteract": "^2.0.6",
|
"@push.rocks/smartinteract": "^2.0.6",
|
||||||
"@push.rocks/smartjson": "^6.0.0",
|
"@push.rocks/smartjson": "^6.0.0",
|
||||||
"@push.rocks/smartjwt": "^2.2.1",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.1.10",
|
"@push.rocks/smartlog": "^3.2.2",
|
||||||
"@push.rocks/smartmail": "^2.2.0",
|
"@push.rocks/smartmail": "^2.2.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartstate": "^2.0.27",
|
"@push.rocks/smartstate": "^2.3.0",
|
||||||
"@push.rocks/smarttime": "^4.1.1",
|
"@push.rocks/smarttime": "^4.2.3",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/smarturl": "^3.1.0",
|
"@push.rocks/smarturl": "^3.1.0",
|
||||||
"@push.rocks/taskbuffer": "^4.1.1",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@push.rocks/webjwt": "^1.0.9",
|
"@push.rocks/webjwt": "^1.0.9",
|
||||||
"@push.rocks/websetup": "^3.0.15",
|
"@push.rocks/websetup": "^3.0.15",
|
||||||
"@push.rocks/webstore": "^2.0.20",
|
"@push.rocks/webstore": "^2.0.21",
|
||||||
"@serve.zone/platformclient": "^1.1.2",
|
"@serve.zone/platformclient": "^1.1.2",
|
||||||
"@tsclass/tsclass": "^9.3.0",
|
"@tsclass/tsclass": "^9.5.0",
|
||||||
"@uptime.link/webwidget": "^1.2.6"
|
"@uptime.link/webwidget": "^1.2.6",
|
||||||
|
"argon2": "^0.44.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.1.2",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsbundle": "^2.8.3",
|
"@git.zone/tsbundle": "^2.10.0",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.2",
|
||||||
"@git.zone/tswatch": "^3.0.1",
|
"@git.zone/tstest": "^3.6.3",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@git.zone/tswatch": "^3.3.2",
|
||||||
"@types/node": "^25.1.0"
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
|
"@types/node": "^25.6.0"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
Generated
+3714
-2091
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
import { LoginSession } from '../ts/reception/classes.loginsession.js';
|
||||||
|
import { User } from '../ts/reception/classes.user.js';
|
||||||
|
import * as plugins from '../ts/plugins.js';
|
||||||
|
|
||||||
|
const createTestLoginSession = () => {
|
||||||
|
const loginSession = new LoginSession();
|
||||||
|
loginSession.id = 'test-session';
|
||||||
|
loginSession.data.userId = 'test-user';
|
||||||
|
(loginSession as LoginSession & { save: () => Promise<void> }).save = async () => undefined;
|
||||||
|
return loginSession;
|
||||||
|
};
|
||||||
|
|
||||||
|
tap.test('hashes passwords with argon2 and verifies them', async () => {
|
||||||
|
const passwordHash = await User.hashPassword('correct horse battery staple');
|
||||||
|
|
||||||
|
expect(passwordHash.startsWith('$argon2')).toBeTrue();
|
||||||
|
expect(await User.verifyPassword('correct horse battery staple', passwordHash)).toBeTrue();
|
||||||
|
expect(await User.verifyPassword('wrong password', passwordHash)).toBeFalse();
|
||||||
|
expect(User.shouldUpgradePasswordHash(passwordHash)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('accepts legacy sha256 hashes and marks them for upgrade', async () => {
|
||||||
|
const legacyHash = await plugins.smarthash.sha256FromString('legacy-password');
|
||||||
|
|
||||||
|
expect(User.isLegacyPasswordHash(legacyHash)).toBeTrue();
|
||||||
|
expect(await User.verifyPassword('legacy-password', legacyHash)).toBeTrue();
|
||||||
|
expect(await User.verifyPassword('different-password', legacyHash)).toBeFalse();
|
||||||
|
expect(User.shouldUpgradePasswordHash(legacyHash)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('rotates refresh tokens and detects reuse', async () => {
|
||||||
|
const loginSession = createTestLoginSession();
|
||||||
|
|
||||||
|
const firstRefreshToken = await loginSession.getRefreshToken();
|
||||||
|
const secondRefreshToken = await loginSession.getRefreshToken();
|
||||||
|
|
||||||
|
expect(firstRefreshToken.startsWith('refresh_')).toBeTrue();
|
||||||
|
expect(secondRefreshToken.startsWith('refresh_')).toBeTrue();
|
||||||
|
expect(firstRefreshToken).not.toEqual(secondRefreshToken);
|
||||||
|
expect(loginSession.data.refreshToken).toBeNullOrUndefined();
|
||||||
|
expect(loginSession.data.refreshTokenHash).toBeTruthy();
|
||||||
|
expect(await loginSession.validateRefreshToken(secondRefreshToken)).toEqual('current');
|
||||||
|
expect(await loginSession.validateRefreshToken(firstRefreshToken)).toEqual('reused');
|
||||||
|
|
||||||
|
await loginSession.invalidate();
|
||||||
|
expect(await loginSession.validateRefreshToken(secondRefreshToken)).toEqual('invalidated');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('persists transfer tokens as one-time hashes', async () => {
|
||||||
|
const loginSession = createTestLoginSession();
|
||||||
|
const transferToken = await loginSession.getTransferToken();
|
||||||
|
|
||||||
|
expect(transferToken.startsWith('transfer_')).toBeTrue();
|
||||||
|
expect(loginSession.data.transferTokenHash).toBeTruthy();
|
||||||
|
expect(await loginSession.validateTransferToken(transferToken)).toBeTrue();
|
||||||
|
expect(await loginSession.validateTransferToken(transferToken)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@idp.global/idp.global',
|
name: '@idp.global/idp.global',
|
||||||
version: '1.16.0',
|
version: '1.17.0',
|
||||||
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,7 @@
|
|||||||
// Native scope
|
// Native scope
|
||||||
|
import * as crypto from 'node:crypto';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
export { path };
|
export { crypto, path };
|
||||||
|
|
||||||
// Project scope
|
// Project scope
|
||||||
import * as idpInterfaces from '../dist_ts_interfaces/index.js';
|
import * as idpInterfaces from '../dist_ts_interfaces/index.js';
|
||||||
@@ -32,8 +33,10 @@ import * as smartpromise from '@push.rocks/smartpromise';
|
|||||||
import * as smarttime from '@push.rocks/smarttime';
|
import * as smarttime from '@push.rocks/smarttime';
|
||||||
import * as smartunique from '@push.rocks/smartunique';
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||||
|
import * as argon2 from 'argon2';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
argon2,
|
||||||
lik,
|
lik,
|
||||||
projectinfo,
|
projectinfo,
|
||||||
qenv,
|
qenv,
|
||||||
|
|||||||
+38
-16
@@ -1,5 +1,6 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { JwtManager } from './classes.jwtmanager.js';
|
import { JwtManager } from './classes.jwtmanager.js';
|
||||||
|
import type { LoginSession } from './classes.loginsession.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a User is identified by its username or email.
|
* a User is identified by its username or email.
|
||||||
@@ -11,21 +12,27 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
|
|||||||
public static async createJwtForRefreshToken(
|
public static async createJwtForRefreshToken(
|
||||||
jwtManagerInstance: JwtManager,
|
jwtManagerInstance: JwtManager,
|
||||||
refreshTokenArg: string
|
refreshTokenArg: string
|
||||||
) {
|
): Promise<string | null> {
|
||||||
const loginSession =
|
const sessionLookup =
|
||||||
await jwtManagerInstance.receptionRef.loginSessionManager.CLoginSession.getLoginSessionByRefreshToken(
|
await jwtManagerInstance.receptionRef.loginSessionManager.findLoginSessionByRefreshToken(
|
||||||
refreshTokenArg
|
refreshTokenArg
|
||||||
);
|
);
|
||||||
if (!loginSession) {
|
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const refreshTokenValid = await loginSession.validateRefreshToken(refreshTokenArg);
|
|
||||||
if (!refreshTokenValid) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return this.createJwtForLoginSession(jwtManagerInstance, sessionLookup.loginSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createJwtForLoginSession(
|
||||||
|
jwtManagerInstance: JwtManager,
|
||||||
|
loginSession: LoginSession
|
||||||
|
): Promise<string | null> {
|
||||||
const user = await jwtManagerInstance.receptionRef.userManager.CUser.getInstance({
|
const user = await jwtManagerInstance.receptionRef.userManager.CUser.getInstance({
|
||||||
id: loginSession.data.userId,
|
id: loginSession.data.userId,
|
||||||
});
|
});
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const validUntil = plugins.smarttime.ExtendedDate.fromMillis(
|
const validUntil = plugins.smarttime.ExtendedDate.fromMillis(
|
||||||
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 1 })
|
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 1 })
|
||||||
);
|
);
|
||||||
@@ -33,10 +40,10 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
|
|||||||
jwt.id = plugins.smartunique.shortId();
|
jwt.id = plugins.smartunique.shortId();
|
||||||
jwt.data = {
|
jwt.data = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
sessionId: loginSession.id,
|
||||||
validUntil: validUntil.getTime(),
|
validUntil: validUntil.getTime(),
|
||||||
refreshEvery: 1000000,
|
refreshEvery: 1000000,
|
||||||
refreshFrom: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 0.5 }),
|
refreshFrom: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 0.5 }),
|
||||||
refreshToken: await loginSession.getRefreshToken(), // TODO: handle multiple refresh tokens
|
|
||||||
justForLooks: {
|
justForLooks: {
|
||||||
validUntilIsoString: validUntil.toISOString(),
|
validUntilIsoString: validUntil.toISOString(),
|
||||||
}
|
}
|
||||||
@@ -46,7 +53,7 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
|
|||||||
|
|
||||||
const jwtString = await jwtManagerInstance.smartjwtInstance.createJWT({
|
const jwtString = await jwtManagerInstance.smartjwtInstance.createJWT({
|
||||||
id: jwt.id,
|
id: jwt.id,
|
||||||
blocked: null,
|
blocked: false,
|
||||||
data: jwt.data,
|
data: jwt.data,
|
||||||
} as plugins.idpInterfaces.data.IJwt);
|
} as plugins.idpInterfaces.data.IJwt);
|
||||||
return jwtString;
|
return jwtString;
|
||||||
@@ -68,11 +75,26 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getLoginSession() {
|
public async getLoginSession() {
|
||||||
const loginSession = await this.manager.receptionRef.loginSessionManager.CLoginSession.getInstance({
|
if (this.data.sessionId) {
|
||||||
data: {
|
return this.manager.receptionRef.loginSessionManager.CLoginSession.getInstance({
|
||||||
refreshToken: this.data.refreshToken,
|
id: this.data.sessionId,
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
return loginSession;
|
|
||||||
|
if (!this.data.refreshToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionLookup =
|
||||||
|
await this.manager.receptionRef.loginSessionManager.findLoginSessionByRefreshToken(
|
||||||
|
this.data.refreshToken
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sessionLookup) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionLookup.loginSession;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,41 @@ export class JwtManager {
|
|||||||
new plugins.typedrequest.TypedHandler(
|
new plugins.typedrequest.TypedHandler(
|
||||||
'refreshJwt',
|
'refreshJwt',
|
||||||
async (requestArg) => {
|
async (requestArg) => {
|
||||||
const resultJwt = await Jwt.createJwtForRefreshToken(this, requestArg.refreshToken);
|
const sessionLookup =
|
||||||
|
await this.receptionRef.loginSessionManager.findLoginSessionByRefreshToken(
|
||||||
|
requestArg.refreshToken
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sessionLookup || sessionLookup.validationStatus === 'invalid') {
|
||||||
|
return {
|
||||||
|
status: 'not found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionLookup.validationStatus === 'invalidated') {
|
||||||
|
return {
|
||||||
|
status: 'invalidated',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionLookup.validationStatus === 'reused') {
|
||||||
|
await sessionLookup.loginSession.invalidate();
|
||||||
|
return {
|
||||||
|
status: 'invalidated',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotatedRefreshToken = await sessionLookup.loginSession.getRefreshToken();
|
||||||
|
const resultJwt = await Jwt.createJwtForLoginSession(this, sessionLookup.loginSession);
|
||||||
|
if (!rotatedRefreshToken || !resultJwt) {
|
||||||
|
return {
|
||||||
|
status: 'invalidated',
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: 'loggedIn',
|
status: 'loggedIn',
|
||||||
jwt: resultJwt,
|
jwt: resultJwt,
|
||||||
|
refreshToken: rotatedRefreshToken,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -120,19 +151,24 @@ export class JwtManager {
|
|||||||
await this.pushPublicKeyToClients();
|
await this.pushPublicKeyToClients();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async verifyJWTAndGetData(jwtArg: string): Promise<Jwt> {
|
public async verifyJWTAndGetData(jwtArg: string): Promise<Jwt | null> {
|
||||||
const jwtData: plugins.idpInterfaces.data.IJwt = await this.smartjwtInstance.verifyJWTAndGetData(jwtArg);
|
const jwtData: plugins.idpInterfaces.data.IJwt = await this.smartjwtInstance.verifyJWTAndGetData(jwtArg);
|
||||||
const jwt = await this.CJwt.getInstance({
|
const jwt = await this.CJwt.getInstance({
|
||||||
id: jwtData.id,
|
id: jwtData.id,
|
||||||
});
|
});
|
||||||
|
if (!jwt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (jwt.blocked) {
|
if (jwt.blocked) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
const loginSession = await jwt.getLoginSession();
|
const loginSession = await jwt.getLoginSession();
|
||||||
if (!loginSession) {
|
if (!loginSession || loginSession.data.invalidated) {
|
||||||
await jwt.block();
|
await jwt.block();
|
||||||
this.blockedJwtIdList.push(jwt.id);
|
if (!this.blockedJwtIdList.includes(jwt.id)) {
|
||||||
|
this.blockedJwtIdList.push(jwt.id);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import * as plugins from '../plugins.js';
|
|||||||
import { LoginSessionManager } from './classes.loginsessionmanager.js';
|
import { LoginSessionManager } from './classes.loginsessionmanager.js';
|
||||||
import { User } from './classes.user.js';
|
import { User } from './classes.user.js';
|
||||||
|
|
||||||
|
export type TRefreshTokenValidationResult = 'current' | 'invalid' | 'invalidated' | 'reused';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a LoginSession keeps track of a login over the whole time of the user being loggedin
|
* a LoginSession keeps track of a login over the whole time of the user being loggedin
|
||||||
*/
|
*/
|
||||||
@@ -40,7 +42,14 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async getLoginSessionByRefreshToken(refreshTokenArg: string) {
|
public static async getLoginSessionByRefreshToken(refreshTokenArg: string) {
|
||||||
const loginSession = await LoginSession.getInstance({
|
const refreshTokenHash = await LoginSession.hashSessionToken(refreshTokenArg);
|
||||||
|
let loginSession = await LoginSession.getInstance({
|
||||||
|
'data.refreshTokenHash': refreshTokenHash,
|
||||||
|
});
|
||||||
|
if (loginSession) {
|
||||||
|
return loginSession;
|
||||||
|
}
|
||||||
|
loginSession = await LoginSession.getInstance({
|
||||||
data: {
|
data: {
|
||||||
refreshToken: refreshTokenArg,
|
refreshToken: refreshTokenArg,
|
||||||
},
|
},
|
||||||
@@ -48,6 +57,14 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
return loginSession;
|
return loginSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async hashSessionToken(tokenArg: string) {
|
||||||
|
return plugins.smarthash.sha256FromString(tokenArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createOpaqueToken(prefixArg: string) {
|
||||||
|
return `${prefixArg}${plugins.crypto.randomBytes(32).toString('base64url')}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ========
|
// ========
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
// ========
|
// ========
|
||||||
@@ -60,13 +77,17 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ weeks: 1 }),
|
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ weeks: 1 }),
|
||||||
invalidated: false,
|
invalidated: false,
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
|
refreshTokenHash: null,
|
||||||
|
rotatedRefreshTokenHashes: [],
|
||||||
|
transferTokenHash: null,
|
||||||
|
transferTokenExpiresAt: null,
|
||||||
deviceId: null,
|
deviceId: null,
|
||||||
deviceInfo: null,
|
deviceInfo: null,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
lastActive: Date.now(),
|
lastActive: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
public transferToken: string;
|
public transferToken: string | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -77,40 +98,99 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
*/
|
*/
|
||||||
public async invalidate() {
|
public async invalidate() {
|
||||||
this.data.invalidated = true;
|
this.data.invalidated = true;
|
||||||
|
this.data.refreshToken = null;
|
||||||
|
this.data.refreshTokenHash = null;
|
||||||
|
this.data.transferTokenHash = null;
|
||||||
|
this.data.transferTokenExpiresAt = null;
|
||||||
await this.save();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a refresh token is unique to a login session and ONLY created once per login session
|
* a refresh token is unique to a login session and rotated whenever it is issued
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public async getRefreshToken() {
|
public async getRefreshToken() {
|
||||||
if (this.data.invalidated) {
|
if (this.data.invalidated) {
|
||||||
console.log('login session is invalidated. no refresh token can be generated.');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!this.data.refreshToken) {
|
const previousRefreshTokenHash =
|
||||||
this.data.refreshToken = plugins.smartunique.uni('refresh_');
|
this.data.refreshTokenHash ||
|
||||||
|
(this.data.refreshToken
|
||||||
|
? await LoginSession.hashSessionToken(this.data.refreshToken)
|
||||||
|
: null);
|
||||||
|
|
||||||
|
if (previousRefreshTokenHash) {
|
||||||
|
this.data.rotatedRefreshTokenHashes = [
|
||||||
|
...(this.data.rotatedRefreshTokenHashes || []),
|
||||||
|
previousRefreshTokenHash,
|
||||||
|
].slice(-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshToken = LoginSession.createOpaqueToken('refresh_');
|
||||||
|
this.data.refreshTokenHash = await LoginSession.hashSessionToken(refreshToken);
|
||||||
|
this.data.refreshToken = null;
|
||||||
|
this.data.lastActive = Date.now();
|
||||||
await this.save();
|
await this.save();
|
||||||
return this.data.refreshToken;
|
return refreshToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTransferToken() {
|
public async getTransferToken() {
|
||||||
this.transferToken = plugins.smartunique.uni('transfer_');
|
this.transferToken = LoginSession.createOpaqueToken('transfer_');
|
||||||
|
this.data.transferTokenHash = await LoginSession.hashSessionToken(this.transferToken);
|
||||||
|
this.data.transferTokenExpiresAt =
|
||||||
|
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ minutes: 5 });
|
||||||
|
await this.save();
|
||||||
return this.transferToken;
|
return this.transferToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async validateRefreshToken(refreshTokenArg: string) {
|
public async validateRefreshToken(
|
||||||
return this.data.refreshToken === refreshTokenArg;
|
refreshTokenArg: string
|
||||||
|
): Promise<TRefreshTokenValidationResult> {
|
||||||
|
if (this.data.invalidated) {
|
||||||
|
return 'invalidated';
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTokenHash = await LoginSession.hashSessionToken(refreshTokenArg);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.data.refreshTokenHash === refreshTokenHash ||
|
||||||
|
(!!this.data.refreshToken && this.data.refreshToken === refreshTokenArg)
|
||||||
|
) {
|
||||||
|
return 'current';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.data.rotatedRefreshTokenHashes || []).includes(refreshTokenHash)) {
|
||||||
|
return 'reused';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'invalid';
|
||||||
}
|
}
|
||||||
|
|
||||||
public async validateTransferToken(transferTokenArg: string) {
|
public async validateTransferToken(transferTokenArg: string) {
|
||||||
const result = this.transferToken === transferTokenArg;
|
if (this.data.invalidated || !this.data.transferTokenHash) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.data.transferTokenExpiresAt &&
|
||||||
|
this.data.transferTokenExpiresAt < Date.now()
|
||||||
|
) {
|
||||||
|
this.data.transferTokenHash = null;
|
||||||
|
this.data.transferTokenExpiresAt = null;
|
||||||
|
await this.save();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result =
|
||||||
|
this.data.transferTokenHash ===
|
||||||
|
(await LoginSession.hashSessionToken(transferTokenArg));
|
||||||
|
|
||||||
// a transfer token can only be used once, so we invalidate it here
|
// a transfer token can only be used once, so we invalidate it here
|
||||||
if (result) {
|
if (result) {
|
||||||
this.transferToken = null;
|
this.transferToken = null;
|
||||||
|
this.data.transferTokenHash = null;
|
||||||
|
this.data.transferTokenExpiresAt = null;
|
||||||
|
await this.save();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { LoginSession } from './classes.loginsession.js';
|
import { LoginSession, type TRefreshTokenValidationResult } from './classes.loginsession.js';
|
||||||
import { Reception } from './classes.reception.js';
|
import { Reception } from './classes.reception.js';
|
||||||
import { logger } from './logging.js';
|
import { logger } from './logging.js';
|
||||||
|
|
||||||
@@ -32,9 +32,6 @@ export class LoginSessionManager {
|
|||||||
let user = await this.receptionRef.userManager.CUser.getInstance({
|
let user = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
data: {
|
data: {
|
||||||
username: requestData.username,
|
username: requestData.username,
|
||||||
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
|
|
||||||
requestData.password
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,33 +39,30 @@ export class LoginSessionManager {
|
|||||||
user = await this.receptionRef.userManager.CUser.getInstance({
|
user = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
data: {
|
data: {
|
||||||
email: requestData.username,
|
email: requestData.username,
|
||||||
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
|
|
||||||
requestData.password
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
if (user && (await this.receptionRef.userManager.CUser.verifyPassword(
|
||||||
// lets recheck
|
requestData.password,
|
||||||
if (
|
user.data.passwordHash
|
||||||
(user.data.username !== requestData.username &&
|
))) {
|
||||||
user.data.email !== requestData.username) ||
|
if (this.receptionRef.userManager.CUser.shouldUpgradePasswordHash(user.data.passwordHash)) {
|
||||||
user.data.passwordHash !==
|
user.data.passwordHash = await this.receptionRef.userManager.CUser.hashPassword(
|
||||||
(await this.receptionRef.userManager.CUser.hashPassword(requestData.password))
|
requestData.password
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'database returned a user that does not match wanted criterea. CRITICAL!'
|
|
||||||
);
|
);
|
||||||
|
await user.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||||
this.loginSessions.add(loginSession);
|
this.loginSessions.add(loginSession);
|
||||||
const refreshToken = await loginSession.getRefreshToken();
|
const refreshToken = await loginSession.getRefreshToken();
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
refreshToken,
|
||||||
refreshToken: refreshToken,
|
|
||||||
twoFaNeeded: false,
|
twoFaNeeded: false,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -109,12 +103,14 @@ export class LoginSessionManager {
|
|||||||
} else {
|
} else {
|
||||||
logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`);
|
logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`);
|
||||||
}
|
}
|
||||||
|
const testOnlyToken =
|
||||||
|
process.env.TEST_MODE && existingUser
|
||||||
|
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
|
||||||
|
?.token
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
testOnlyToken: process.env.TEST_MODE
|
testOnlyToken,
|
||||||
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
|
|
||||||
.token
|
|
||||||
: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -133,10 +129,17 @@ export class LoginSessionManager {
|
|||||||
email: requestArg.email,
|
email: requestArg.email,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||||
|
}
|
||||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||||
this.loginSessions.add(loginSession);
|
this.loginSessions.add(loginSession);
|
||||||
|
const refreshToken = await loginSession.getRefreshToken();
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
refreshToken: await loginSession.getRefreshToken(),
|
refreshToken,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new plugins.typedrequest.TypedResponseError('Validation Token not found');
|
throw new plugins.typedrequest.TypedResponseError('Validation Token not found');
|
||||||
@@ -147,8 +150,11 @@ export class LoginSessionManager {
|
|||||||
|
|
||||||
this.typedRouter.addTypedHandler<plugins.idpInterfaces.request.ILogoutRequest>(
|
this.typedRouter.addTypedHandler<plugins.idpInterfaces.request.ILogoutRequest>(
|
||||||
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
|
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
|
||||||
const loginSession = await this.CLoginSession.getLoginSessionByRefreshToken(requestDataArg.refreshToken);
|
const sessionLookup = await this.findLoginSessionByRefreshToken(requestDataArg.refreshToken);
|
||||||
await loginSession.invalidate();
|
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Invalid refresh token');
|
||||||
|
}
|
||||||
|
await sessionLookup.loginSession.invalidate();
|
||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -158,31 +164,39 @@ export class LoginSessionManager {
|
|||||||
'exchangeRefreshTokenAndTransferToken',
|
'exchangeRefreshTokenAndTransferToken',
|
||||||
async (requestDataArg) => {
|
async (requestDataArg) => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case !!requestDataArg.refreshToken:
|
case !!requestDataArg.refreshToken: {
|
||||||
const loginSession = await this.loginSessions.find(async (loginSessionArg) => {
|
const sessionLookup = await this.findLoginSessionByRefreshToken(
|
||||||
return loginSessionArg.validateRefreshToken(requestDataArg.refreshToken);
|
requestDataArg.refreshToken
|
||||||
});
|
);
|
||||||
if (!loginSession) {
|
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
|
||||||
|
if (sessionLookup?.validationStatus === 'reused') {
|
||||||
|
await sessionLookup.loginSession.invalidate();
|
||||||
|
}
|
||||||
throw new plugins.typedrequest.TypedResponseError('your refresh token is invalid');
|
throw new plugins.typedrequest.TypedResponseError('your refresh token is invalid');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
transferToken: await loginSession.getTransferToken(),
|
transferToken: await sessionLookup.loginSession.getTransferToken(),
|
||||||
};
|
};
|
||||||
break;
|
}
|
||||||
case !!requestDataArg.transferToken:
|
case !!requestDataArg.transferToken: {
|
||||||
let transferToken: string;
|
const loginSession2 = await this.findLoginSessionByTransferToken(
|
||||||
const loginSession2 = await this.loginSessions.find(async (loginSessionArg) => {
|
requestDataArg.transferToken
|
||||||
return loginSessionArg.validateTransferToken(requestDataArg.transferToken);
|
);
|
||||||
});
|
|
||||||
if (!loginSession2) {
|
if (!loginSession2) {
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'Your transfer token is not valid.'
|
'Your transfer token is not valid.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const refreshToken = await loginSession2.getRefreshToken();
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
refreshToken: await loginSession2.getRefreshToken(),
|
refreshToken,
|
||||||
};
|
};
|
||||||
break;
|
}
|
||||||
|
default:
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Invalid token exchange request');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -271,8 +285,7 @@ export class LoginSessionManager {
|
|||||||
throw new plugins.typedrequest.TypedResponseError('Invalid JWT');
|
throw new plugins.typedrequest.TypedResponseError('Invalid JWT');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current session's refresh token to identify the current session
|
const currentLoginSession = await jwt.getLoginSession();
|
||||||
const currentRefreshToken = jwt.data.refreshToken;
|
|
||||||
|
|
||||||
// Get all sessions for this user
|
// Get all sessions for this user
|
||||||
const sessions = await this.CLoginSession.getInstances({
|
const sessions = await this.CLoginSession.getInstances({
|
||||||
@@ -290,7 +303,7 @@ export class LoginSessionManager {
|
|||||||
ip: session.data.deviceInfo?.ip || 'Unknown',
|
ip: session.data.deviceInfo?.ip || 'Unknown',
|
||||||
lastActive: session.data.lastActive || session.data.createdAt || Date.now(),
|
lastActive: session.data.lastActive || session.data.createdAt || Date.now(),
|
||||||
createdAt: session.data.createdAt || Date.now(),
|
createdAt: session.data.createdAt || Date.now(),
|
||||||
isCurrent: session.data.refreshToken === currentRefreshToken,
|
isCurrent: session.id === currentLoginSession?.id,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -317,8 +330,10 @@ export class LoginSessionManager {
|
|||||||
throw new plugins.typedrequest.TypedResponseError('Session not found');
|
throw new plugins.typedrequest.TypedResponseError('Session not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentLoginSession = await jwt.getLoginSession();
|
||||||
|
|
||||||
// Don't allow revoking the current session via this method
|
// Don't allow revoking the current session via this method
|
||||||
if (sessionToRevoke.data.refreshToken === jwt.data.refreshToken) {
|
if (sessionToRevoke.id === currentLoginSession?.id) {
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'Cannot revoke current session. Use logout instead.'
|
'Cannot revoke current session. Use logout instead.'
|
||||||
);
|
);
|
||||||
@@ -338,4 +353,44 @@ export class LoginSessionManager {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async findLoginSessionByRefreshToken(refreshTokenArg: string): Promise<{
|
||||||
|
loginSession: LoginSession;
|
||||||
|
validationStatus: TRefreshTokenValidationResult;
|
||||||
|
} | null> {
|
||||||
|
const directMatch = await this.CLoginSession.getLoginSessionByRefreshToken(refreshTokenArg);
|
||||||
|
if (directMatch) {
|
||||||
|
return {
|
||||||
|
loginSession: directMatch,
|
||||||
|
validationStatus: await directMatch.validateRefreshToken(refreshTokenArg),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSessions = await this.CLoginSession.getInstances({});
|
||||||
|
for (const loginSession of loginSessions) {
|
||||||
|
const validationStatus = await loginSession.validateRefreshToken(refreshTokenArg);
|
||||||
|
if (validationStatus !== 'invalid') {
|
||||||
|
return {
|
||||||
|
loginSession,
|
||||||
|
validationStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findLoginSessionByTransferToken(transferTokenArg: string) {
|
||||||
|
const transferTokenHash = await LoginSession.hashSessionToken(transferTokenArg);
|
||||||
|
const loginSession = await this.CLoginSession.getInstance({
|
||||||
|
'data.transferTokenHash': transferTokenHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!loginSession) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await loginSession.validateTransferToken(transferTokenArg);
|
||||||
|
return isValid ? loginSession : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import { logger } from './logging.js';
|
import { logger } from './logging.js';
|
||||||
|
|
||||||
import { JwtManager } from './classes.jwtmanager.js';
|
import { JwtManager } from './classes.jwtmanager.js';
|
||||||
@@ -30,7 +29,6 @@ export interface IReceptionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Reception {
|
export class Reception {
|
||||||
public projectinfoNpm = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
public serviceQenv = new plugins.qenv.Qenv('./', './.nogit');
|
public serviceQenv = new plugins.qenv.Qenv('./', './.nogit');
|
||||||
public szPlatformClient = new plugins.szPlatformClient.SzPlatformClient();
|
public szPlatformClient = new plugins.szPlatformClient.SzPlatformClient();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export class ReceptionDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
console.log(this.receptionRef.options.mongoDescriptor);
|
|
||||||
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.receptionRef.options.mongoDescriptor);
|
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.receptionRef.options.mongoDescriptor);
|
||||||
await this.smartdataDb.init();
|
await this.smartdataDb.init();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
const newUser = new User();
|
const newUser = new User();
|
||||||
newUser.id = plugins.smartunique.shortId();
|
newUser.id = plugins.smartunique.shortId();
|
||||||
newUser.data = {
|
newUser.data = {
|
||||||
connectedOrgs: null,
|
connectedOrgs: [],
|
||||||
status: 'new',
|
status: 'new',
|
||||||
name: userDataArg.name,
|
name: userDataArg.name,
|
||||||
username: userDataArg.username,
|
username: userDataArg.username,
|
||||||
@@ -31,8 +31,26 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static hashPassword(passwordArg: string) {
|
public static async hashPassword(passwordArg: string) {
|
||||||
return plugins.smarthash.sha256FromString(passwordArg);
|
return plugins.argon2.hash(passwordArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isLegacyPasswordHash(passwordHashArg?: string) {
|
||||||
|
return !!passwordHashArg && !passwordHashArg.startsWith('$argon2');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static shouldUpgradePasswordHash(passwordHashArg?: string) {
|
||||||
|
return this.isLegacyPasswordHash(passwordHashArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async verifyPassword(passwordArg: string, passwordHashArg?: string) {
|
||||||
|
if (!passwordHashArg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.isLegacyPasswordHash(passwordHashArg)) {
|
||||||
|
return passwordHashArg === (await plugins.smarthash.sha256FromString(passwordArg));
|
||||||
|
}
|
||||||
|
return plugins.argon2.verify(passwordHashArg, passwordArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export class UserManager {
|
|||||||
new plugins.typedrequest.TypedHandler('getRolesAndOrganizationsForUserId', async reqArg => {
|
new plugins.typedrequest.TypedHandler('getRolesAndOrganizationsForUserId', async reqArg => {
|
||||||
console.log('user manager: getting roles and orgs');
|
console.log('user manager: getting roles and orgs');
|
||||||
const user = await this.getUserByJwtValidation(reqArg.jwt);
|
const user = await this.getUserByJwtValidation(reqArg.jwt);
|
||||||
|
if (!user) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||||
|
}
|
||||||
const organizations = await this.receptionRef.organizationmanager.getAllOrganizationsForUser(
|
const organizations = await this.receptionRef.organizationmanager.getAllOrganizationsForUser(
|
||||||
user
|
user
|
||||||
);
|
);
|
||||||
@@ -49,8 +52,7 @@ export class UserManager {
|
|||||||
email: user.data.email,
|
email: user.data.email,
|
||||||
mobileNumber: user.data.mobileNumber,
|
mobileNumber: user.data.mobileNumber,
|
||||||
connectedOrgs: user.data.connectedOrgs,
|
connectedOrgs: user.data.connectedOrgs,
|
||||||
status: null,
|
status: user.data.status,
|
||||||
password: null,
|
|
||||||
isGlobalAdmin: user.data.isGlobalAdmin,
|
isGlobalAdmin: user.data.isGlobalAdmin,
|
||||||
} as plugins.idpInterfaces.data.IUser['data']
|
} as plugins.idpInterfaces.data.IUser['data']
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,9 @@ export class UserManager {
|
|||||||
*/
|
*/
|
||||||
public async getUserByJwt(jwtString: string) {
|
public async getUserByJwt(jwtString: string) {
|
||||||
const jwtInstance = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtString);
|
const jwtInstance = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtString);
|
||||||
|
if (!jwtInstance) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const user = await this.CUser.getInstance({
|
const user = await this.CUser.getInstance({
|
||||||
id: jwtInstance.data.userId
|
id: jwtInstance.data.userId
|
||||||
});
|
});
|
||||||
@@ -75,7 +80,10 @@ export class UserManager {
|
|||||||
* faster than the "getUserByJwt"
|
* faster than the "getUserByJwt"
|
||||||
*/
|
*/
|
||||||
public async getUserByJwtValidation(jwtStringArg: string) {
|
public async getUserByJwtValidation(jwtStringArg: string) {
|
||||||
const jwtDataArg: plugins.idpInterfaces.data.IJwt = await this.receptionRef.jwtManager.smartjwtInstance.verifyJWTAndGetData(jwtStringArg);
|
const jwtDataArg = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtStringArg);
|
||||||
|
if (!jwtDataArg) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const resultingUser = await this.CUser.getInstance({
|
const resultingUser = await this.CUser.getInstance({
|
||||||
id: jwtDataArg.data.userId
|
id: jwtDataArg.data.userId
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
const projectinfoNpm = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
|
||||||
|
|
||||||
export const logger = new plugins.smartlog.ConsoleLog();
|
export const logger = new plugins.smartlog.ConsoleLog();
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ export class IdpCli {
|
|||||||
this.storeCredentials({
|
this.storeCredentials({
|
||||||
...credentials,
|
...credentials,
|
||||||
jwt: response.jwt,
|
jwt: response.jwt,
|
||||||
|
refreshToken: response.refreshToken || credentials.refreshToken,
|
||||||
});
|
});
|
||||||
return response.jwt;
|
return response.jwt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export class IdpClient {
|
|||||||
appDataArg = {
|
appDataArg = {
|
||||||
id: '', // TODO
|
id: '', // TODO
|
||||||
appUrl: `https://${window.location.host}/`,
|
appUrl: `https://${window.location.host}/`,
|
||||||
description: null,
|
description: '',
|
||||||
logoUrl: null,
|
logoUrl: '',
|
||||||
name: null,
|
name: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.appData = appDataArg;
|
this.appData = appDataArg;
|
||||||
@@ -67,10 +67,14 @@ export class IdpClient {
|
|||||||
await this.storeJwt(jwtStringArg);
|
await this.storeJwt(jwtStringArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async setRefreshToken(refreshTokenArg: string) {
|
||||||
|
await this.storeRefreshToken(refreshTokenArg);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a typedsocket for going reactive
|
* a typedsocket for going reactive
|
||||||
*/
|
*/
|
||||||
public typedsocket: plugins.typedsocket.TypedSocket;
|
public typedsocket!: plugins.typedsocket.TypedSocket;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a typed router to go reactive
|
* a typed router to go reactive
|
||||||
@@ -89,16 +93,30 @@ export class IdpClient {
|
|||||||
await this.ssoStore.set('idpJwt', jwtString);
|
await this.ssoStore.set('idpJwt', jwtString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async storeRefreshToken(refreshToken: string) {
|
||||||
|
await this.ssoStore.set('idpRefreshToken', refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
public async getJwt(): Promise<string> {
|
public async getJwt(): Promise<string> {
|
||||||
return await this.ssoStore.get('idpJwt');
|
return await this.ssoStore.get('idpJwt');
|
||||||
}
|
}
|
||||||
|
public async getRefreshToken(): Promise<string> {
|
||||||
|
return await this.ssoStore.get('idpRefreshToken');
|
||||||
|
}
|
||||||
public async getJwtData(): Promise<plugins.idpInterfaces.data.IJwt> {
|
public async getJwtData(): Promise<plugins.idpInterfaces.data.IJwt> {
|
||||||
return this.helpers.extractDataFromJwtString(await this.getJwt());
|
return this.helpers.extractDataFromJwtString(await this.getJwt());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteJwt() {
|
public async deleteJwt() {
|
||||||
await this.ssoStore.delete('idpJwt');
|
await this.ssoStore.delete('idpJwt');
|
||||||
console.log('removed jwt');
|
}
|
||||||
|
|
||||||
|
public async deleteRefreshToken() {
|
||||||
|
await this.ssoStore.delete('idpRefreshToken');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async clearAuthState() {
|
||||||
|
await Promise.all([this.deleteJwt(), this.deleteRefreshToken()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,47 +133,63 @@ export class IdpClient {
|
|||||||
if (extractedJwt.data.refreshFrom < Date.now() && Date.now() < extractedJwt.data.validUntil) {
|
if (extractedJwt.data.refreshFrom < Date.now() && Date.now() < extractedJwt.data.validUntil) {
|
||||||
jwt = await this.refreshJwt();
|
jwt = await this.refreshJwt();
|
||||||
} else if (Date.now() > extractedJwt.data.validUntil) {
|
} else if (Date.now() > extractedJwt.data.validUntil) {
|
||||||
this.deleteJwt();
|
await this.deleteJwt();
|
||||||
|
jwt = await this.refreshJwt();
|
||||||
}
|
}
|
||||||
return jwt;
|
return jwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refreshJwt(refreshTokenArg?: string): Promise<string> {
|
public async refreshJwt(refreshTokenArg?: string): Promise<string | null> {
|
||||||
let extractedJwt: plugins.idpInterfaces.data.IJwt;
|
const refreshToken = refreshTokenArg || (await this.getRefreshToken());
|
||||||
|
|
||||||
if (!refreshTokenArg) {
|
if (!refreshToken) {
|
||||||
extractedJwt = await this.helpers.extractDataFromJwtString(await this.getJwt());
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.typedsocketDeferred.promise;
|
await this.typedsocketDeferred.promise;
|
||||||
const refreshJwtReq =
|
const refreshJwtReq =
|
||||||
this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
|
this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
|
||||||
'refreshJwt'
|
'refreshJwt'
|
||||||
);
|
);
|
||||||
const response = await refreshJwtReq.fire({
|
const response = await refreshJwtReq
|
||||||
refreshToken: refreshTokenArg || extractedJwt.data.refreshToken,
|
.fire({
|
||||||
});
|
refreshToken,
|
||||||
if (response.jwt) {
|
})
|
||||||
await this.storeJwt(response.jwt);
|
.catch(async () => {
|
||||||
} else {
|
await this.clearAuthState();
|
||||||
await this.deleteJwt();
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response?.jwt) {
|
||||||
|
await this.clearAuthState();
|
||||||
|
this.statusObservable.next(response?.status || 'loggedOut');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.refreshToken) {
|
||||||
|
await this.storeRefreshToken(response.refreshToken);
|
||||||
|
}
|
||||||
|
await this.storeJwt(response.jwt);
|
||||||
this.statusObservable.next(response.status);
|
this.statusObservable.next(response.status);
|
||||||
return await this.getJwt();
|
return response.jwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* can be used to switch between pages
|
* can be used to switch between pages
|
||||||
*/
|
*/
|
||||||
public async getTransferToken(appDataArg?: plugins.idpInterfaces.data.IAppLegacy): Promise<string> {
|
public async getTransferToken(appDataArg?: plugins.idpInterfaces.data.IAppLegacy): Promise<string | null> {
|
||||||
const jwt = await this.performJwtHousekeeping();
|
await this.performJwtHousekeeping();
|
||||||
const extractedJwt = await this.helpers.extractDataFromJwtString(jwt);
|
const refreshToken = await this.getRefreshToken();
|
||||||
|
if (!refreshToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
await this.typedsocketDeferred.promise;
|
await this.typedsocketDeferred.promise;
|
||||||
const getTransferToken =
|
const getTransferToken =
|
||||||
this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ExchangeRefreshTokenAndTransferToken>(
|
this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ExchangeRefreshTokenAndTransferToken>(
|
||||||
'exchangeRefreshTokenAndTransferToken'
|
'exchangeRefreshTokenAndTransferToken'
|
||||||
);
|
);
|
||||||
const response = await getTransferToken.fire({
|
const response = await getTransferToken.fire({
|
||||||
refreshToken: extractedJwt.data.refreshToken,
|
refreshToken,
|
||||||
appData: appDataArg || this.appData,
|
appData: appDataArg || this.appData,
|
||||||
});
|
});
|
||||||
return response.transferToken;
|
return response.transferToken;
|
||||||
@@ -230,6 +264,13 @@ export class IdpClient {
|
|||||||
const jwt = await this.performJwtHousekeeping();
|
const jwt = await this.performJwtHousekeeping();
|
||||||
return !!jwt;
|
return !!jwt;
|
||||||
} else {
|
} else {
|
||||||
|
const refreshToken = await this.getRefreshToken();
|
||||||
|
if (refreshToken) {
|
||||||
|
const jwt = await this.refreshJwt(refreshToken);
|
||||||
|
if (jwt) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
const transferTokenResult = await this.processTransferToken();
|
const transferTokenResult = await this.processTransferToken();
|
||||||
if (transferTokenResult) {
|
if (transferTokenResult) {
|
||||||
// we are in the clear
|
// we are in the clear
|
||||||
@@ -258,12 +299,18 @@ export class IdpClient {
|
|||||||
*/
|
*/
|
||||||
public async logout() {
|
public async logout() {
|
||||||
const idpLogoutUrl = this.parsedReceptionUrl.clone().set('path', '/logout');
|
const idpLogoutUrl = this.parsedReceptionUrl.clone().set('path', '/logout');
|
||||||
|
const refreshToken = await this.getRefreshToken();
|
||||||
if (!globalThis.location.href.startsWith(idpLogoutUrl.origin)) {
|
if (!globalThis.location.href.startsWith(idpLogoutUrl.origin)) {
|
||||||
// we are somewhere in an app
|
// we are somewhere in an app
|
||||||
await this.deleteJwt();
|
await this.clearAuthState();
|
||||||
globalThis.location.href = idpLogoutUrl.toString();
|
globalThis.location.href = idpLogoutUrl.toString();
|
||||||
} else {
|
} else {
|
||||||
// we are in the sso page
|
// we are in the sso page
|
||||||
|
if (!refreshToken) {
|
||||||
|
await this.clearAuthState();
|
||||||
|
window.location.href = this.parsedReceptionUrl.origin;
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.enableTypedSocket();
|
await this.enableTypedSocket();
|
||||||
console.log(`logging out against ${this.parsedReceptionUrl.toString()}`);
|
console.log(`logging out against ${this.parsedReceptionUrl.toString()}`);
|
||||||
const logoutTr =
|
const logoutTr =
|
||||||
@@ -271,9 +318,9 @@ export class IdpClient {
|
|||||||
'logout'
|
'logout'
|
||||||
);
|
);
|
||||||
await logoutTr.fire({
|
await logoutTr.fire({
|
||||||
refreshToken: (await this.getJwtData()).data.refreshToken,
|
refreshToken,
|
||||||
});
|
});
|
||||||
await this.deleteJwt();
|
await this.clearAuthState();
|
||||||
const appData = await this.getAppDataOnSsoDomain();
|
const appData = await this.getAppDataOnSsoDomain();
|
||||||
if (appData) {
|
if (appData) {
|
||||||
console.log(`redirecting to app after logout: ${appData.appUrl}`);
|
console.log(`redirecting to app after logout: ${appData.appUrl}`);
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ export interface IJwt {
|
|||||||
*/
|
*/
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the login session backing this jwt
|
||||||
|
*/
|
||||||
|
sessionId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the latest point of
|
* the latest point of
|
||||||
*/
|
*/
|
||||||
@@ -24,9 +29,9 @@ export interface IJwt {
|
|||||||
refreshEvery: number;
|
refreshEvery: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the refresh token to obtain a new jwt for a session
|
* legacy field kept for compatibility with already-issued jwt documents
|
||||||
*/
|
*/
|
||||||
refreshToken: string;
|
refreshToken?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* just for looks/debugging
|
* just for looks/debugging
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
export interface ILoginSession {
|
export interface ILoginSession {
|
||||||
id: string;
|
id: string;
|
||||||
data: {
|
data: {
|
||||||
userId: string;
|
userId: string | null;
|
||||||
validUntil: number;
|
validUntil: number;
|
||||||
invalidated: boolean;
|
invalidated: boolean;
|
||||||
refreshToken: string;
|
/**
|
||||||
|
* legacy plaintext refresh token field kept so existing sessions can migrate on first use
|
||||||
|
*/
|
||||||
|
refreshToken?: string | null;
|
||||||
|
refreshTokenHash?: string | null;
|
||||||
|
rotatedRefreshTokenHashes?: string[];
|
||||||
|
transferTokenHash?: string | null;
|
||||||
|
transferTokenExpiresAt?: number | null;
|
||||||
/**
|
/**
|
||||||
* a device id that can be used to share the login session
|
* a device id that can be used to share the login session
|
||||||
* in different contexts on the same device
|
* in different contexts on the same device
|
||||||
*/
|
*/
|
||||||
deviceId: string;
|
deviceId?: string | null;
|
||||||
/**
|
/**
|
||||||
* Device metadata for session display
|
* Device metadata for session display
|
||||||
*/
|
*/
|
||||||
@@ -18,7 +25,7 @@ export interface ILoginSession {
|
|||||||
browser: string;
|
browser: string;
|
||||||
os: string;
|
os: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
};
|
} | null;
|
||||||
/**
|
/**
|
||||||
* When this session was created
|
* When this session was created
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ export interface IReq_RefreshJwt
|
|||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
status: data.TLoginStatus;
|
status: data.TLoginStatus;
|
||||||
jwt: string;
|
jwt?: string;
|
||||||
|
refreshToken?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@idp.global/idp.global',
|
name: '@idp.global/idp.global',
|
||||||
version: '1.16.0',
|
version: '1.17.0',
|
||||||
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { accountDesignTokens } from './sharedstyles.js';
|
|||||||
import * as views from './views/index.js';
|
import * as views from './views/index.js';
|
||||||
import * as accountstate from '../../states/accountstate.js';
|
import * as accountstate from '../../states/accountstate.js';
|
||||||
|
|
||||||
import { commitinfo } from '../../../dist_ts/00_commitinfo_data.js';
|
import { commitinfo } from '../../../ts/00_commitinfo_data.js';
|
||||||
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { accountDesignTokens } from './sharedstyles.js';
|
|||||||
import { CreateOrgModal } from './create-org-modal.js';
|
import { CreateOrgModal } from './create-org-modal.js';
|
||||||
import { OrgSelectModal } from './org-select-modal.js';
|
import { OrgSelectModal } from './org-select-modal.js';
|
||||||
|
|
||||||
import { commitinfo } from '../../../dist_ts/00_commitinfo_data.js';
|
import { commitinfo } from '../../../ts/00_commitinfo_data.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
query,
|
query,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import { commitinfo } from '../../dist_ts/00_commitinfo_data.js';
|
import { commitinfo } from '../../ts/00_commitinfo_data.js';
|
||||||
import { IdpState } from '../states/idp.state.js';
|
import { IdpState } from '../states/idp.state.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -207,21 +207,14 @@ export class IdpRegistrationPrompt extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handleRefreshToken(refreshTokenArg: string, delayDispatchMillisArg = 0) {
|
public async handleRefreshToken(refreshTokenArg: string, delayDispatchMillisArg = 0) {
|
||||||
// a refreshToken binds directly to a session.
|
|
||||||
// the refresh token is used on a continuous basis to get fresh and short-lived jwts
|
|
||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const refreshJwt = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
|
const jwt = await idpState.idpClient.refreshJwt(refreshTokenArg);
|
||||||
'refreshJwt'
|
|
||||||
);
|
|
||||||
const responseJwt = await refreshJwt.fire({
|
|
||||||
refreshToken: refreshTokenArg,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (responseJwt.jwt) {
|
if (jwt) {
|
||||||
this.domtools.convenience.smartdelay.delayFor(delayDispatchMillisArg).then(() => {
|
this.domtools.convenience.smartdelay.delayFor(delayDispatchMillisArg).then(() => {
|
||||||
this.dispatchJwt(responseJwt.jwt);
|
this.dispatchJwt(jwt);
|
||||||
});
|
});
|
||||||
return responseJwt.jwt;
|
return jwt;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,15 +488,15 @@ export class IdpRegistrationStepper extends DeesElement {
|
|||||||
username: this.storedData.email,
|
username: this.storedData.email,
|
||||||
password: eventArg.detail.data.password,
|
password: eventArg.detail.data.password,
|
||||||
});
|
});
|
||||||
this.storedData.refreshToken = loginResponse.refreshToken;
|
|
||||||
|
|
||||||
deesForm.setStatus('pending', 'Obtaining JWT...');
|
deesForm.setStatus('pending', 'Obtaining JWT...');
|
||||||
const jwtResponse = await idpState.idpClient.requests.obtainJwt.fire({
|
const jwt = await idpState.idpClient.refreshJwt(loginResponse.refreshToken);
|
||||||
refreshToken: this.storedData.refreshToken,
|
|
||||||
});
|
if (!jwt) {
|
||||||
|
deesForm.setStatus('error', 'Failed to establish a login session.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
deesForm.setStatus('success', 'Ok! Lets Go!');
|
deesForm.setStatus('success', 'Ok! Lets Go!');
|
||||||
await idpState.idpClient.setJwt(jwtResponse.jwt);
|
|
||||||
idpState.domtools.router.pushUrl('/account');
|
idpState.domtools.router.pushUrl('/account');
|
||||||
}, { signal });
|
}, { signal });
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-1
@@ -4,7 +4,9 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": false
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"dist_*/**/*.d.ts"
|
"dist_*/**/*.d.ts"
|
||||||
|
|||||||
Reference in New Issue
Block a user