Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d1e6ea6e1 | |||
| 98e614a945 |
@@ -1,5 +1,14 @@
|
||||
# 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)
|
||||
add local development docs, update tswatch preset and add Playwright screenshots
|
||||
|
||||
|
||||
+26
-24
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@idp.global/idp.global",
|
||||
"version": "1.16.0",
|
||||
"version": "1.17.0",
|
||||
"description": "An identity provider software managing user authentications, registrations, and sessions.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "npm run build",
|
||||
"test": "pnpm run build && tstest test/",
|
||||
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle",
|
||||
"watch": "tswatch",
|
||||
"start": "(node cli.js)",
|
||||
@@ -16,49 +16,51 @@
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.2.5",
|
||||
"@api.global/typedrequest": "^3.3.0",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||
"@api.global/typedserver": "^8.3.0",
|
||||
"@api.global/typedsocket": "^4.1.0",
|
||||
"@api.global/typedserver": "^8.4.6",
|
||||
"@api.global/typedsocket": "^4.1.2",
|
||||
"@consent.software/catalog": "^2.0.1",
|
||||
"@design.estate/dees-catalog": "^3.41.4",
|
||||
"@design.estate/dees-domtools": "^2.3.8",
|
||||
"@design.estate/dees-element": "^2.1.6",
|
||||
"@git.zone/tspublish": "^1.11.0",
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@design.estate/dees-catalog": "^3.81.0",
|
||||
"@design.estate/dees-domtools": "^2.5.4",
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@git.zone/tspublish": "^1.11.5",
|
||||
"@push.rocks/lik": "^6.4.0",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@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/smartfile": "^13.1.0",
|
||||
"@push.rocks/smarthash": "^3.2.6",
|
||||
"@push.rocks/smartinteract": "^2.0.6",
|
||||
"@push.rocks/smartjson": "^6.0.0",
|
||||
"@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/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartstate": "^2.0.27",
|
||||
"@push.rocks/smarttime": "^4.1.1",
|
||||
"@push.rocks/smartstate": "^2.3.0",
|
||||
"@push.rocks/smarttime": "^4.2.3",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@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/websetup": "^3.0.15",
|
||||
"@push.rocks/webstore": "^2.0.20",
|
||||
"@push.rocks/webstore": "^2.0.21",
|
||||
"@serve.zone/platformclient": "^1.1.2",
|
||||
"@tsclass/tsclass": "^9.3.0",
|
||||
"@uptime.link/webwidget": "^1.2.6"
|
||||
"@tsclass/tsclass": "^9.5.0",
|
||||
"@uptime.link/webwidget": "^1.2.6",
|
||||
"argon2": "^0.44.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tsbundle": "^2.8.3",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tswatch": "^3.0.1",
|
||||
"@push.rocks/projectinfo": "^5.0.1",
|
||||
"@types/node": "^25.1.0"
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsbundle": "^2.10.0",
|
||||
"@git.zone/tsrun": "^2.0.2",
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@git.zone/tswatch": "^3.3.2",
|
||||
"@push.rocks/projectinfo": "^5.1.0",
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"private": true,
|
||||
"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 = {
|
||||
name: '@idp.global/idp.global',
|
||||
version: '1.16.0',
|
||||
version: '1.17.0',
|
||||
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
// Native scope
|
||||
import * as crypto from 'node:crypto';
|
||||
import * as path from 'path';
|
||||
export { path };
|
||||
export { crypto, path };
|
||||
|
||||
// Project scope
|
||||
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 smartunique from '@push.rocks/smartunique';
|
||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||
import * as argon2 from 'argon2';
|
||||
|
||||
export {
|
||||
argon2,
|
||||
lik,
|
||||
projectinfo,
|
||||
qenv,
|
||||
|
||||
+36
-14
@@ -1,5 +1,6 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { JwtManager } from './classes.jwtmanager.js';
|
||||
import type { LoginSession } from './classes.loginsession.js';
|
||||
|
||||
/**
|
||||
* 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(
|
||||
jwtManagerInstance: JwtManager,
|
||||
refreshTokenArg: string
|
||||
) {
|
||||
const loginSession =
|
||||
await jwtManagerInstance.receptionRef.loginSessionManager.CLoginSession.getLoginSessionByRefreshToken(
|
||||
): Promise<string | null> {
|
||||
const sessionLookup =
|
||||
await jwtManagerInstance.receptionRef.loginSessionManager.findLoginSessionByRefreshToken(
|
||||
refreshTokenArg
|
||||
);
|
||||
if (!loginSession) {
|
||||
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
|
||||
return null;
|
||||
}
|
||||
const refreshTokenValid = await loginSession.validateRefreshToken(refreshTokenArg);
|
||||
if (!refreshTokenValid) {
|
||||
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({
|
||||
id: loginSession.data.userId,
|
||||
});
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
const validUntil = plugins.smarttime.ExtendedDate.fromMillis(
|
||||
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.data = {
|
||||
userId: user.id,
|
||||
sessionId: loginSession.id,
|
||||
validUntil: validUntil.getTime(),
|
||||
refreshEvery: 1000000,
|
||||
refreshFrom: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 0.5 }),
|
||||
refreshToken: await loginSession.getRefreshToken(), // TODO: handle multiple refresh tokens
|
||||
justForLooks: {
|
||||
validUntilIsoString: validUntil.toISOString(),
|
||||
}
|
||||
@@ -46,7 +53,7 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
|
||||
|
||||
const jwtString = await jwtManagerInstance.smartjwtInstance.createJWT({
|
||||
id: jwt.id,
|
||||
blocked: null,
|
||||
blocked: false,
|
||||
data: jwt.data,
|
||||
} as plugins.idpInterfaces.data.IJwt);
|
||||
return jwtString;
|
||||
@@ -68,11 +75,26 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
|
||||
}
|
||||
|
||||
public async getLoginSession() {
|
||||
const loginSession = await this.manager.receptionRef.loginSessionManager.CLoginSession.getInstance({
|
||||
data: {
|
||||
refreshToken: this.data.refreshToken,
|
||||
}
|
||||
if (this.data.sessionId) {
|
||||
return this.manager.receptionRef.loginSessionManager.CLoginSession.getInstance({
|
||||
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(
|
||||
'refreshJwt',
|
||||
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 {
|
||||
status: 'loggedIn',
|
||||
jwt: resultJwt,
|
||||
refreshToken: rotatedRefreshToken,
|
||||
};
|
||||
}
|
||||
)
|
||||
@@ -120,19 +151,24 @@ export class JwtManager {
|
||||
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 jwt = await this.CJwt.getInstance({
|
||||
id: jwtData.id,
|
||||
});
|
||||
if (!jwt) {
|
||||
return null;
|
||||
}
|
||||
if (jwt.blocked) {
|
||||
return null;
|
||||
}
|
||||
if (jwt) {
|
||||
const loginSession = await jwt.getLoginSession();
|
||||
if (!loginSession) {
|
||||
if (!loginSession || loginSession.data.invalidated) {
|
||||
await jwt.block();
|
||||
if (!this.blockedJwtIdList.includes(jwt.id)) {
|
||||
this.blockedJwtIdList.push(jwt.id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import * as plugins from '../plugins.js';
|
||||
import { LoginSessionManager } from './classes.loginsessionmanager.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
|
||||
*/
|
||||
@@ -40,7 +42,14 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
||||
}
|
||||
|
||||
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: {
|
||||
refreshToken: refreshTokenArg,
|
||||
},
|
||||
@@ -48,6 +57,14 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
||||
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
|
||||
// ========
|
||||
@@ -60,13 +77,17 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
||||
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ weeks: 1 }),
|
||||
invalidated: false,
|
||||
refreshToken: null,
|
||||
refreshTokenHash: null,
|
||||
rotatedRefreshTokenHashes: [],
|
||||
transferTokenHash: null,
|
||||
transferTokenExpiresAt: null,
|
||||
deviceId: null,
|
||||
deviceInfo: null,
|
||||
createdAt: Date.now(),
|
||||
lastActive: Date.now(),
|
||||
};
|
||||
|
||||
public transferToken: string;
|
||||
public transferToken: string | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -77,40 +98,99 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
||||
*/
|
||||
public async invalidate() {
|
||||
this.data.invalidated = true;
|
||||
this.data.refreshToken = null;
|
||||
this.data.refreshTokenHash = null;
|
||||
this.data.transferTokenHash = null;
|
||||
this.data.transferTokenExpiresAt = null;
|
||||
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
|
||||
*/
|
||||
public async getRefreshToken() {
|
||||
if (this.data.invalidated) {
|
||||
console.log('login session is invalidated. no refresh token can be generated.');
|
||||
return null;
|
||||
}
|
||||
if (!this.data.refreshToken) {
|
||||
this.data.refreshToken = plugins.smartunique.uni('refresh_');
|
||||
const previousRefreshTokenHash =
|
||||
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();
|
||||
return this.data.refreshToken;
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public async validateRefreshToken(refreshTokenArg: string) {
|
||||
return this.data.refreshToken === refreshTokenArg;
|
||||
public async validateRefreshToken(
|
||||
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) {
|
||||
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
|
||||
if (result) {
|
||||
this.transferToken = null;
|
||||
this.data.transferTokenHash = null;
|
||||
this.data.transferTokenExpiresAt = null;
|
||||
await this.save();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { logger } from './logging.js';
|
||||
|
||||
@@ -32,9 +32,6 @@ export class LoginSessionManager {
|
||||
let user = await this.receptionRef.userManager.CUser.getInstance({
|
||||
data: {
|
||||
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({
|
||||
data: {
|
||||
email: requestData.username,
|
||||
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
|
||||
requestData.password
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user) {
|
||||
// lets recheck
|
||||
if (
|
||||
(user.data.username !== requestData.username &&
|
||||
user.data.email !== requestData.username) ||
|
||||
user.data.passwordHash !==
|
||||
(await this.receptionRef.userManager.CUser.hashPassword(requestData.password))
|
||||
) {
|
||||
throw new Error(
|
||||
'database returned a user that does not match wanted criterea. CRITICAL!'
|
||||
if (user && (await this.receptionRef.userManager.CUser.verifyPassword(
|
||||
requestData.password,
|
||||
user.data.passwordHash
|
||||
))) {
|
||||
if (this.receptionRef.userManager.CUser.shouldUpgradePasswordHash(user.data.passwordHash)) {
|
||||
user.data.passwordHash = await this.receptionRef.userManager.CUser.hashPassword(
|
||||
requestData.password
|
||||
);
|
||||
await user.save();
|
||||
}
|
||||
|
||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||
this.loginSessions.add(loginSession);
|
||||
const refreshToken = await loginSession.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
refreshToken: refreshToken,
|
||||
refreshToken,
|
||||
twoFaNeeded: false,
|
||||
};
|
||||
} else {
|
||||
@@ -109,12 +103,14 @@ export class LoginSessionManager {
|
||||
} else {
|
||||
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 {
|
||||
status: 'ok',
|
||||
testOnlyToken: process.env.TEST_MODE
|
||||
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
|
||||
.token
|
||||
: null,
|
||||
testOnlyToken,
|
||||
};
|
||||
}
|
||||
)
|
||||
@@ -133,10 +129,17 @@ export class LoginSessionManager {
|
||||
email: requestArg.email,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||
}
|
||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||
this.loginSessions.add(loginSession);
|
||||
const refreshToken = await loginSession.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||
}
|
||||
return {
|
||||
refreshToken: await loginSession.getRefreshToken(),
|
||||
refreshToken,
|
||||
};
|
||||
} else {
|
||||
throw new plugins.typedrequest.TypedResponseError('Validation Token not found');
|
||||
@@ -147,8 +150,11 @@ export class LoginSessionManager {
|
||||
|
||||
this.typedRouter.addTypedHandler<plugins.idpInterfaces.request.ILogoutRequest>(
|
||||
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
|
||||
const loginSession = await this.CLoginSession.getLoginSessionByRefreshToken(requestDataArg.refreshToken);
|
||||
await loginSession.invalidate();
|
||||
const sessionLookup = await this.findLoginSessionByRefreshToken(requestDataArg.refreshToken);
|
||||
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
|
||||
throw new plugins.typedrequest.TypedResponseError('Invalid refresh token');
|
||||
}
|
||||
await sessionLookup.loginSession.invalidate();
|
||||
return {}
|
||||
})
|
||||
);
|
||||
@@ -158,31 +164,39 @@ export class LoginSessionManager {
|
||||
'exchangeRefreshTokenAndTransferToken',
|
||||
async (requestDataArg) => {
|
||||
switch (true) {
|
||||
case !!requestDataArg.refreshToken:
|
||||
const loginSession = await this.loginSessions.find(async (loginSessionArg) => {
|
||||
return loginSessionArg.validateRefreshToken(requestDataArg.refreshToken);
|
||||
});
|
||||
if (!loginSession) {
|
||||
case !!requestDataArg.refreshToken: {
|
||||
const sessionLookup = await this.findLoginSessionByRefreshToken(
|
||||
requestDataArg.refreshToken
|
||||
);
|
||||
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
|
||||
if (sessionLookup?.validationStatus === 'reused') {
|
||||
await sessionLookup.loginSession.invalidate();
|
||||
}
|
||||
throw new plugins.typedrequest.TypedResponseError('your refresh token is invalid');
|
||||
}
|
||||
return {
|
||||
transferToken: await loginSession.getTransferToken(),
|
||||
transferToken: await sessionLookup.loginSession.getTransferToken(),
|
||||
};
|
||||
break;
|
||||
case !!requestDataArg.transferToken:
|
||||
let transferToken: string;
|
||||
const loginSession2 = await this.loginSessions.find(async (loginSessionArg) => {
|
||||
return loginSessionArg.validateTransferToken(requestDataArg.transferToken);
|
||||
});
|
||||
}
|
||||
case !!requestDataArg.transferToken: {
|
||||
const loginSession2 = await this.findLoginSessionByTransferToken(
|
||||
requestDataArg.transferToken
|
||||
);
|
||||
if (!loginSession2) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Your transfer token is not valid.'
|
||||
);
|
||||
}
|
||||
const refreshToken = await loginSession2.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||
}
|
||||
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');
|
||||
}
|
||||
|
||||
// Get the current session's refresh token to identify the current session
|
||||
const currentRefreshToken = jwt.data.refreshToken;
|
||||
const currentLoginSession = await jwt.getLoginSession();
|
||||
|
||||
// Get all sessions for this user
|
||||
const sessions = await this.CLoginSession.getInstances({
|
||||
@@ -290,7 +303,7 @@ export class LoginSessionManager {
|
||||
ip: session.data.deviceInfo?.ip || 'Unknown',
|
||||
lastActive: session.data.lastActive || 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');
|
||||
}
|
||||
|
||||
const currentLoginSession = await jwt.getLoginSession();
|
||||
|
||||
// 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(
|
||||
'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 paths from '../paths.js';
|
||||
import { logger } from './logging.js';
|
||||
|
||||
import { JwtManager } from './classes.jwtmanager.js';
|
||||
@@ -30,7 +29,6 @@ export interface IReceptionOptions {
|
||||
}
|
||||
|
||||
export class Reception {
|
||||
public projectinfoNpm = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
public serviceQenv = new plugins.qenv.Qenv('./', './.nogit');
|
||||
public szPlatformClient = new plugins.szPlatformClient.SzPlatformClient();
|
||||
|
||||
@@ -10,7 +10,6 @@ export class ReceptionDb {
|
||||
}
|
||||
|
||||
public async start() {
|
||||
console.log(this.receptionRef.options.mongoDescriptor);
|
||||
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.receptionRef.options.mongoDescriptor);
|
||||
await this.smartdataDb.init();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
|
||||
const newUser = new User();
|
||||
newUser.id = plugins.smartunique.shortId();
|
||||
newUser.data = {
|
||||
connectedOrgs: null,
|
||||
connectedOrgs: [],
|
||||
status: 'new',
|
||||
name: userDataArg.name,
|
||||
username: userDataArg.username,
|
||||
@@ -31,8 +31,26 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
|
||||
return newUser;
|
||||
}
|
||||
|
||||
public static hashPassword(passwordArg: string) {
|
||||
return plugins.smarthash.sha256FromString(passwordArg);
|
||||
public static async hashPassword(passwordArg: string) {
|
||||
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
|
||||
|
||||
@@ -23,6 +23,9 @@ export class UserManager {
|
||||
new plugins.typedrequest.TypedHandler('getRolesAndOrganizationsForUserId', async reqArg => {
|
||||
console.log('user manager: getting roles and orgs');
|
||||
const user = await this.getUserByJwtValidation(reqArg.jwt);
|
||||
if (!user) {
|
||||
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||
}
|
||||
const organizations = await this.receptionRef.organizationmanager.getAllOrganizationsForUser(
|
||||
user
|
||||
);
|
||||
@@ -49,8 +52,7 @@ export class UserManager {
|
||||
email: user.data.email,
|
||||
mobileNumber: user.data.mobileNumber,
|
||||
connectedOrgs: user.data.connectedOrgs,
|
||||
status: null,
|
||||
password: null,
|
||||
status: user.data.status,
|
||||
isGlobalAdmin: user.data.isGlobalAdmin,
|
||||
} as plugins.idpInterfaces.data.IUser['data']
|
||||
}
|
||||
@@ -64,6 +66,9 @@ export class UserManager {
|
||||
*/
|
||||
public async getUserByJwt(jwtString: string) {
|
||||
const jwtInstance = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtString);
|
||||
if (!jwtInstance) {
|
||||
return null;
|
||||
}
|
||||
const user = await this.CUser.getInstance({
|
||||
id: jwtInstance.data.userId
|
||||
});
|
||||
@@ -75,7 +80,10 @@ export class UserManager {
|
||||
* faster than the "getUserByJwt"
|
||||
*/
|
||||
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({
|
||||
id: jwtDataArg.data.userId
|
||||
});
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
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();
|
||||
|
||||
@@ -193,6 +193,7 @@ export class IdpCli {
|
||||
this.storeCredentials({
|
||||
...credentials,
|
||||
jwt: response.jwt,
|
||||
refreshToken: response.refreshToken || credentials.refreshToken,
|
||||
});
|
||||
return response.jwt;
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ export class IdpClient {
|
||||
appDataArg = {
|
||||
id: '', // TODO
|
||||
appUrl: `https://${window.location.host}/`,
|
||||
description: null,
|
||||
logoUrl: null,
|
||||
name: null,
|
||||
description: '',
|
||||
logoUrl: '',
|
||||
name: '',
|
||||
};
|
||||
}
|
||||
this.appData = appDataArg;
|
||||
@@ -67,10 +67,14 @@ export class IdpClient {
|
||||
await this.storeJwt(jwtStringArg);
|
||||
}
|
||||
|
||||
public async setRefreshToken(refreshTokenArg: string) {
|
||||
await this.storeRefreshToken(refreshTokenArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* a typedsocket for going reactive
|
||||
*/
|
||||
public typedsocket: plugins.typedsocket.TypedSocket;
|
||||
public typedsocket!: plugins.typedsocket.TypedSocket;
|
||||
|
||||
/**
|
||||
* a typed router to go reactive
|
||||
@@ -89,16 +93,30 @@ export class IdpClient {
|
||||
await this.ssoStore.set('idpJwt', jwtString);
|
||||
}
|
||||
|
||||
public async storeRefreshToken(refreshToken: string) {
|
||||
await this.ssoStore.set('idpRefreshToken', refreshToken);
|
||||
}
|
||||
|
||||
public async getJwt(): Promise<string> {
|
||||
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> {
|
||||
return this.helpers.extractDataFromJwtString(await this.getJwt());
|
||||
}
|
||||
|
||||
public async deleteJwt() {
|
||||
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) {
|
||||
jwt = await this.refreshJwt();
|
||||
} else if (Date.now() > extractedJwt.data.validUntil) {
|
||||
this.deleteJwt();
|
||||
await this.deleteJwt();
|
||||
jwt = await this.refreshJwt();
|
||||
}
|
||||
return jwt;
|
||||
}
|
||||
|
||||
public async refreshJwt(refreshTokenArg?: string): Promise<string> {
|
||||
let extractedJwt: plugins.idpInterfaces.data.IJwt;
|
||||
public async refreshJwt(refreshTokenArg?: string): Promise<string | null> {
|
||||
const refreshToken = refreshTokenArg || (await this.getRefreshToken());
|
||||
|
||||
if (!refreshTokenArg) {
|
||||
extractedJwt = await this.helpers.extractDataFromJwtString(await this.getJwt());
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.typedsocketDeferred.promise;
|
||||
const refreshJwtReq =
|
||||
this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
|
||||
'refreshJwt'
|
||||
);
|
||||
const response = await refreshJwtReq.fire({
|
||||
refreshToken: refreshTokenArg || extractedJwt.data.refreshToken,
|
||||
const response = await refreshJwtReq
|
||||
.fire({
|
||||
refreshToken,
|
||||
})
|
||||
.catch(async () => {
|
||||
await this.clearAuthState();
|
||||
return null;
|
||||
});
|
||||
if (response.jwt) {
|
||||
await this.storeJwt(response.jwt);
|
||||
} else {
|
||||
await this.deleteJwt();
|
||||
|
||||
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);
|
||||
return await this.getJwt();
|
||||
return response.jwt;
|
||||
}
|
||||
|
||||
/**
|
||||
* can be used to switch between pages
|
||||
*/
|
||||
public async getTransferToken(appDataArg?: plugins.idpInterfaces.data.IAppLegacy): Promise<string> {
|
||||
const jwt = await this.performJwtHousekeeping();
|
||||
const extractedJwt = await this.helpers.extractDataFromJwtString(jwt);
|
||||
public async getTransferToken(appDataArg?: plugins.idpInterfaces.data.IAppLegacy): Promise<string | null> {
|
||||
await this.performJwtHousekeeping();
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
await this.typedsocketDeferred.promise;
|
||||
const getTransferToken =
|
||||
this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ExchangeRefreshTokenAndTransferToken>(
|
||||
'exchangeRefreshTokenAndTransferToken'
|
||||
);
|
||||
const response = await getTransferToken.fire({
|
||||
refreshToken: extractedJwt.data.refreshToken,
|
||||
refreshToken,
|
||||
appData: appDataArg || this.appData,
|
||||
});
|
||||
return response.transferToken;
|
||||
@@ -230,6 +264,13 @@ export class IdpClient {
|
||||
const jwt = await this.performJwtHousekeeping();
|
||||
return !!jwt;
|
||||
} else {
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (refreshToken) {
|
||||
const jwt = await this.refreshJwt(refreshToken);
|
||||
if (jwt) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const transferTokenResult = await this.processTransferToken();
|
||||
if (transferTokenResult) {
|
||||
// we are in the clear
|
||||
@@ -258,12 +299,18 @@ export class IdpClient {
|
||||
*/
|
||||
public async logout() {
|
||||
const idpLogoutUrl = this.parsedReceptionUrl.clone().set('path', '/logout');
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (!globalThis.location.href.startsWith(idpLogoutUrl.origin)) {
|
||||
// we are somewhere in an app
|
||||
await this.deleteJwt();
|
||||
await this.clearAuthState();
|
||||
globalThis.location.href = idpLogoutUrl.toString();
|
||||
} else {
|
||||
// we are in the sso page
|
||||
if (!refreshToken) {
|
||||
await this.clearAuthState();
|
||||
window.location.href = this.parsedReceptionUrl.origin;
|
||||
return;
|
||||
}
|
||||
await this.enableTypedSocket();
|
||||
console.log(`logging out against ${this.parsedReceptionUrl.toString()}`);
|
||||
const logoutTr =
|
||||
@@ -271,9 +318,9 @@ export class IdpClient {
|
||||
'logout'
|
||||
);
|
||||
await logoutTr.fire({
|
||||
refreshToken: (await this.getJwtData()).data.refreshToken,
|
||||
refreshToken,
|
||||
});
|
||||
await this.deleteJwt();
|
||||
await this.clearAuthState();
|
||||
const appData = await this.getAppDataOnSsoDomain();
|
||||
if (appData) {
|
||||
console.log(`redirecting to app after logout: ${appData.appUrl}`);
|
||||
|
||||
@@ -10,6 +10,11 @@ export interface IJwt {
|
||||
*/
|
||||
userId: string;
|
||||
|
||||
/**
|
||||
* the login session backing this jwt
|
||||
*/
|
||||
sessionId?: string;
|
||||
|
||||
/**
|
||||
* the latest point of
|
||||
*/
|
||||
@@ -24,9 +29,9 @@ export interface IJwt {
|
||||
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
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
export interface ILoginSession {
|
||||
id: string;
|
||||
data: {
|
||||
userId: string;
|
||||
userId: string | null;
|
||||
validUntil: number;
|
||||
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
|
||||
* in different contexts on the same device
|
||||
*/
|
||||
deviceId: string;
|
||||
deviceId?: string | null;
|
||||
/**
|
||||
* Device metadata for session display
|
||||
*/
|
||||
@@ -18,7 +25,7 @@ export interface ILoginSession {
|
||||
browser: string;
|
||||
os: string;
|
||||
ip: string;
|
||||
};
|
||||
} | null;
|
||||
/**
|
||||
* When this session was created
|
||||
*/
|
||||
|
||||
@@ -87,7 +87,8 @@ export interface IReq_RefreshJwt
|
||||
};
|
||||
response: {
|
||||
status: data.TLoginStatus;
|
||||
jwt: string;
|
||||
jwt?: string;
|
||||
refreshToken?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@idp.global/idp.global',
|
||||
version: '1.16.0',
|
||||
version: '1.17.0',
|
||||
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 accountstate from '../../states/accountstate.js';
|
||||
|
||||
import { commitinfo } from '../../../dist_ts/00_commitinfo_data.js';
|
||||
import { commitinfo } from '../../../ts/00_commitinfo_data.js';
|
||||
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { accountDesignTokens } from './sharedstyles.js';
|
||||
import { CreateOrgModal } from './create-org-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 {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
query,
|
||||
} 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';
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -207,21 +207,14 @@ export class IdpRegistrationPrompt extends DeesElement {
|
||||
}
|
||||
|
||||
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 refreshJwt = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
|
||||
'refreshJwt'
|
||||
);
|
||||
const responseJwt = await refreshJwt.fire({
|
||||
refreshToken: refreshTokenArg,
|
||||
});
|
||||
const jwt = await idpState.idpClient.refreshJwt(refreshTokenArg);
|
||||
|
||||
if (responseJwt.jwt) {
|
||||
if (jwt) {
|
||||
this.domtools.convenience.smartdelay.delayFor(delayDispatchMillisArg).then(() => {
|
||||
this.dispatchJwt(responseJwt.jwt);
|
||||
this.dispatchJwt(jwt);
|
||||
});
|
||||
return responseJwt.jwt;
|
||||
return jwt;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -488,15 +488,15 @@ export class IdpRegistrationStepper extends DeesElement {
|
||||
username: this.storedData.email,
|
||||
password: eventArg.detail.data.password,
|
||||
});
|
||||
this.storedData.refreshToken = loginResponse.refreshToken;
|
||||
|
||||
deesForm.setStatus('pending', 'Obtaining JWT...');
|
||||
const jwtResponse = await idpState.idpClient.requests.obtainJwt.fire({
|
||||
refreshToken: this.storedData.refreshToken,
|
||||
});
|
||||
const jwt = await idpState.idpClient.refreshJwt(loginResponse.refreshToken);
|
||||
|
||||
if (!jwt) {
|
||||
deesForm.setStatus('error', 'Failed to establish a login session.');
|
||||
return;
|
||||
}
|
||||
|
||||
deesForm.setStatus('success', 'Ok! Lets Go!');
|
||||
await idpState.idpClient.setJwt(jwtResponse.jwt);
|
||||
idpState.domtools.router.pushUrl('/account');
|
||||
}, { signal });
|
||||
},
|
||||
|
||||
+3
-1
@@ -4,7 +4,9 @@
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["node"],
|
||||
"strict": false
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
|
||||
Reference in New Issue
Block a user