Compare commits

..

2 Commits

Author SHA1 Message Date
jkunz 2d1e6ea6e1 v1.17.0
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-20 08:12:07 +00:00
jkunz 98e614a945 feat(auth): harden authentication with argon2 passwords and rotating hashed refresh tokens 2026-04-20 08:12:07 +00:00
27 changed files with 4226 additions and 2259 deletions
+9
View File
@@ -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
+26 -24
View File
@@ -1,12 +1,12 @@
{ {
"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.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"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": {
+3714 -2091
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -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();
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
} }
} }
+40 -4
View File
@@ -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;
} }
} }
+91 -11
View File
@@ -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;
} }
+99 -44
View File
@@ -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;
}
} }
-2
View File
@@ -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();
-1
View File
@@ -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();
} }
+21 -3
View File
@@ -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
+11 -3
View File
@@ -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
}); });
-3
View File
@@ -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();
+1
View File
@@ -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;
} }
+72 -25
View File
@@ -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}`);
+7 -2
View File
@@ -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;
}; };
} }
+1 -1
View File
@@ -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.'
} }
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+4 -11
View File
@@ -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;
} }
+6 -6
View File
@@ -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
View File
@@ -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"