Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d1e6ea6e1 | |||
| 98e614a945 | |||
| ad3e51a9e8 | |||
| d8f72d620a | |||
| 53b36e506c | |||
| 7d5ad29a27 | |||
| 724ec2d134 | |||
| 32ffc1bbaa |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
@@ -1,5 +1,39 @@
|
|||||||
# 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)
|
||||||
|
add local development docs, update tswatch preset and add Playwright screenshots
|
||||||
|
|
||||||
|
- readme.md: added a Local Development section with prerequisites, quick-start commands, environment variables, development routes, and default development credentials + security note
|
||||||
|
- npmextra.json: changed @git.zone/tswatch preset from "website" to "service" and disabled the built-in server (removed port/serveDir/liveReload and set server.enabled false); removed triggerReload from website watcher
|
||||||
|
- .playwright-mcp: added Playwright screenshots (login-page.png, register-page.png, account-dashboard.png) for visual tests / CI
|
||||||
|
|
||||||
|
## 2026-01-29 - 1.15.0 - feat(build)
|
||||||
|
add tsbundle/tswatch configs, update build/watch scripts, bump dependencies, and add CLI documentation
|
||||||
|
|
||||||
|
- Add tsbundle and tswatch configuration to npmextra.json to support bundling and a local dev server (dist_serve, liveReload, watch patterns).
|
||||||
|
- Update package.json build/watch scripts to use generic tsbundle/tswatch invocations (removed explicit 'website' target).
|
||||||
|
- Bump dependencies and devDependencies: @git.zone/tsbuild ^4.0.2 -> ^4.1.2, @git.zone/tsbundle ^2.6.3 -> ^2.8.3, @git.zone/tswatch ^2.3.13 -> ^3.0.1, @api.global/typedserver ^8.1.0 -> ^8.3.0, several @design.estate packages, @push.rocks/taskbuffer ^3.5.0 -> ^4.1.1, @types/node 25.0.3 -> 25.1.0, and other minor/patch bumps.
|
||||||
|
- Add a new CLI README (ts_idpcli/readme.md) with usage, commands, programmatic API examples and configuration.
|
||||||
|
- Update README license/Legal sections in ts_idpclient, ts_interfaces and ts_web to include license, trademark, and company information.
|
||||||
|
|
||||||
|
## 2025-12-22 - 1.14.1 - fix(oidc)
|
||||||
|
migrate OIDC endpoints and internal handlers to use typedserver IRequestContext and update dependencies
|
||||||
|
|
||||||
|
- Updated route handlers in ts/index.ts to pass ctx (IRequestContext) instead of req
|
||||||
|
- Refactored OIDC manager handlers to accept plugins.typedserver.IRequestContext and use ctx.url, ctx.headers, ctx.formData (handleAuthorize, handleToken, handleUserInfo, handleRevoke)
|
||||||
|
- Bumped dependencies to support the new typedserver API: @api.global/typedserver -> ^8.1.0
|
||||||
|
- Other dependency updates: @design.estate/dees-catalog ^3.4.0, @git.zone/tspublish ^1.11.0, @types/node ^25.0.3
|
||||||
|
- Changing public handler method signatures is a breaking API change; recommend a major version bump
|
||||||
|
|
||||||
## 2025-12-16 - 1.14.0 - feat(docs)
|
## 2025-12-16 - 1.14.0 - feat(docs)
|
||||||
add package READMEs and publish metadata; update web package publish order
|
add package READMEs and publish metadata; update web package publish order
|
||||||
|
|
||||||
|
|||||||
@@ -50,5 +50,40 @@
|
|||||||
"registries": ["https://verdaccio.lossless.digital"],
|
"registries": ["https://verdaccio.lossless.digital"],
|
||||||
"accessLevel": "public"
|
"accessLevel": "public"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"@git.zone/tsbundle": {
|
||||||
|
"bundles": [
|
||||||
|
{
|
||||||
|
"from": "./ts_web/index.ts",
|
||||||
|
"to": "./dist_serve/bundle.js",
|
||||||
|
"outputMode": "bundle",
|
||||||
|
"bundler": "esbuild",
|
||||||
|
"production": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@git.zone/tswatch": {
|
||||||
|
"preset": "service",
|
||||||
|
"server": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"watchers": [
|
||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"watch": "./ts/**/*",
|
||||||
|
"command": "npm run startTs",
|
||||||
|
"restart": true,
|
||||||
|
"debounce": 300,
|
||||||
|
"runOnStart": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bundles": [
|
||||||
|
{
|
||||||
|
"name": "website",
|
||||||
|
"from": "./ts_web/index.ts",
|
||||||
|
"to": "./dist_serve/bundle.js",
|
||||||
|
"watchPatterns": ["./ts_web/**/*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-27
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@idp.global/idp.global",
|
"name": "@idp.global/idp.global",
|
||||||
"version": "1.14.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 website --production",
|
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle",
|
||||||
"watch": "tswatch website",
|
"watch": "tswatch",
|
||||||
"start": "(node cli.js)",
|
"start": "(node cli.js)",
|
||||||
"startTs": "(node cli.ts.js)",
|
"startTs": "(node cli.ts.js)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
@@ -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": "^7.11.1",
|
"@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.3.1",
|
"@design.estate/dees-catalog": "^3.81.0",
|
||||||
"@design.estate/dees-domtools": "^2.3.6",
|
"@design.estate/dees-domtools": "^2.5.4",
|
||||||
"@design.estate/dees-element": "^2.1.3",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@git.zone/tspublish": "^1.10.3",
|
"@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.19",
|
"@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": "^3.5.0",
|
"@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.0.2",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsbundle": "^2.6.3",
|
"@git.zone/tsbundle": "^2.10.0",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.2",
|
||||||
"@git.zone/tswatch": "^2.3.13",
|
"@git.zone/tstest": "^3.6.3",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@git.zone/tswatch": "^3.3.2",
|
||||||
"@types/node": "^24.10.1"
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
|
"@types/node": "^25.6.0"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
Generated
+3797
-2071
File diff suppressed because it is too large
Load Diff
@@ -130,6 +130,68 @@ volumes:
|
|||||||
|
|
||||||
The server listens on port 2999 by default.
|
The server listens on port 2999 by default.
|
||||||
|
|
||||||
|
## 🛠️ Local Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 20+
|
||||||
|
- pnpm
|
||||||
|
- MongoDB (local or remote)
|
||||||
|
- SMTP server (for email verification in registration flow)
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://code.foss.global/idp.global/idp.global.git
|
||||||
|
cd idp.global
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Start development server with hot reload
|
||||||
|
pnpm watch
|
||||||
|
```
|
||||||
|
|
||||||
|
The server runs on **http://localhost:2999** with:
|
||||||
|
- 🔄 Auto-restart backend on changes (`ts/`)
|
||||||
|
- 📦 Automatic frontend bundle rebuilding (`ts_web/`)
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
|
||||||
|
Create environment variables for the backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MONGODB_URL=mongodb://localhost:27017/idp-dev
|
||||||
|
export IDP_BASEURL=http://localhost:2999
|
||||||
|
export INSTANCE_NAME=idp-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Routes
|
||||||
|
|
||||||
|
| Route | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `/` | Welcome/landing page |
|
||||||
|
| `/login` | Sign in form |
|
||||||
|
| `/register` | New user registration |
|
||||||
|
| `/account` | User dashboard (requires auth) |
|
||||||
|
|
||||||
|
### 🔑 Default Development Credentials
|
||||||
|
|
||||||
|
For local development with the test database, use:
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Email/Username** | `admin@idp.global` or `admin` |
|
||||||
|
| **Password** | `admin` |
|
||||||
|
|
||||||
|
This account has `isGlobalAdmin: true` for full platform access including the admin panel at `/account/admin`.
|
||||||
|
|
||||||
|
> ⚠️ **Security Note**: These credentials are for local development only. Never use default credentials in production environments.
|
||||||
|
|
||||||
## 📦 Published Packages
|
## 📦 Published Packages
|
||||||
|
|
||||||
This monorepo publishes the following npm packages:
|
This monorepo publishes the following npm packages:
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
import { LoginSession } from '../ts/reception/classes.loginsession.js';
|
||||||
|
import { User } from '../ts/reception/classes.user.js';
|
||||||
|
import * as plugins from '../ts/plugins.js';
|
||||||
|
|
||||||
|
const createTestLoginSession = () => {
|
||||||
|
const loginSession = new LoginSession();
|
||||||
|
loginSession.id = 'test-session';
|
||||||
|
loginSession.data.userId = 'test-user';
|
||||||
|
(loginSession as LoginSession & { save: () => Promise<void> }).save = async () => undefined;
|
||||||
|
return loginSession;
|
||||||
|
};
|
||||||
|
|
||||||
|
tap.test('hashes passwords with argon2 and verifies them', async () => {
|
||||||
|
const passwordHash = await User.hashPassword('correct horse battery staple');
|
||||||
|
|
||||||
|
expect(passwordHash.startsWith('$argon2')).toBeTrue();
|
||||||
|
expect(await User.verifyPassword('correct horse battery staple', passwordHash)).toBeTrue();
|
||||||
|
expect(await User.verifyPassword('wrong password', passwordHash)).toBeFalse();
|
||||||
|
expect(User.shouldUpgradePasswordHash(passwordHash)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('accepts legacy sha256 hashes and marks them for upgrade', async () => {
|
||||||
|
const legacyHash = await plugins.smarthash.sha256FromString('legacy-password');
|
||||||
|
|
||||||
|
expect(User.isLegacyPasswordHash(legacyHash)).toBeTrue();
|
||||||
|
expect(await User.verifyPassword('legacy-password', legacyHash)).toBeTrue();
|
||||||
|
expect(await User.verifyPassword('different-password', legacyHash)).toBeFalse();
|
||||||
|
expect(User.shouldUpgradePasswordHash(legacyHash)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('rotates refresh tokens and detects reuse', async () => {
|
||||||
|
const loginSession = createTestLoginSession();
|
||||||
|
|
||||||
|
const firstRefreshToken = await loginSession.getRefreshToken();
|
||||||
|
const secondRefreshToken = await loginSession.getRefreshToken();
|
||||||
|
|
||||||
|
expect(firstRefreshToken.startsWith('refresh_')).toBeTrue();
|
||||||
|
expect(secondRefreshToken.startsWith('refresh_')).toBeTrue();
|
||||||
|
expect(firstRefreshToken).not.toEqual(secondRefreshToken);
|
||||||
|
expect(loginSession.data.refreshToken).toBeNullOrUndefined();
|
||||||
|
expect(loginSession.data.refreshTokenHash).toBeTruthy();
|
||||||
|
expect(await loginSession.validateRefreshToken(secondRefreshToken)).toEqual('current');
|
||||||
|
expect(await loginSession.validateRefreshToken(firstRefreshToken)).toEqual('reused');
|
||||||
|
|
||||||
|
await loginSession.invalidate();
|
||||||
|
expect(await loginSession.validateRefreshToken(secondRefreshToken)).toEqual('invalidated');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('persists transfer tokens as one-time hashes', async () => {
|
||||||
|
const loginSession = createTestLoginSession();
|
||||||
|
const transferToken = await loginSession.getTransferToken();
|
||||||
|
|
||||||
|
expect(transferToken.startsWith('transfer_')).toBeTrue();
|
||||||
|
expect(loginSession.data.transferTokenHash).toBeTruthy();
|
||||||
|
expect(await loginSession.validateTransferToken(transferToken)).toBeTrue();
|
||||||
|
expect(await loginSession.validateTransferToken(transferToken)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@idp.global/idp.global',
|
name: '@idp.global/idp.global',
|
||||||
version: '1.14.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.'
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -28,40 +28,40 @@ export const runCli = async () => {
|
|||||||
typedserver.options.spaFallback = true;
|
typedserver.options.spaFallback = true;
|
||||||
|
|
||||||
// OIDC Discovery endpoint
|
// OIDC Discovery endpoint
|
||||||
typedserver.addRoute('/.well-known/openid-configuration', 'GET', async (req) => {
|
typedserver.addRoute('/.well-known/openid-configuration', 'GET', async (ctx) => {
|
||||||
return new Response(JSON.stringify(reception.oidcManager.getDiscoveryDocument()), {
|
return new Response(JSON.stringify(reception.oidcManager.getDiscoveryDocument()), {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// JWKS endpoint
|
// JWKS endpoint
|
||||||
typedserver.addRoute('/.well-known/jwks.json', 'GET', async (req) => {
|
typedserver.addRoute('/.well-known/jwks.json', 'GET', async (ctx) => {
|
||||||
return new Response(JSON.stringify(reception.oidcManager.getJwks()), {
|
return new Response(JSON.stringify(reception.oidcManager.getJwks()), {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// OAuth Authorization endpoint
|
// OAuth Authorization endpoint
|
||||||
typedserver.addRoute('/oauth/authorize', 'GET', async (req) => {
|
typedserver.addRoute('/oauth/authorize', 'GET', async (ctx) => {
|
||||||
return reception.oidcManager.handleAuthorize(req);
|
return reception.oidcManager.handleAuthorize(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// OAuth Token endpoint
|
// OAuth Token endpoint
|
||||||
typedserver.addRoute('/oauth/token', 'POST', async (req) => {
|
typedserver.addRoute('/oauth/token', 'POST', async (ctx) => {
|
||||||
return reception.oidcManager.handleToken(req);
|
return reception.oidcManager.handleToken(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// OAuth UserInfo endpoint (GET and POST)
|
// OAuth UserInfo endpoint (GET and POST)
|
||||||
typedserver.addRoute('/oauth/userinfo', 'GET', async (req) => {
|
typedserver.addRoute('/oauth/userinfo', 'GET', async (ctx) => {
|
||||||
return reception.oidcManager.handleUserInfo(req);
|
return reception.oidcManager.handleUserInfo(ctx);
|
||||||
});
|
});
|
||||||
typedserver.addRoute('/oauth/userinfo', 'POST', async (req) => {
|
typedserver.addRoute('/oauth/userinfo', 'POST', async (ctx) => {
|
||||||
return reception.oidcManager.handleUserInfo(req);
|
return reception.oidcManager.handleUserInfo(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// OAuth Revocation endpoint
|
// OAuth Revocation endpoint
|
||||||
typedserver.addRoute('/oauth/revoke', 'POST', async (req) => {
|
typedserver.addRoute('/oauth/revoke', 'POST', async (ctx) => {
|
||||||
return reception.oidcManager.handleRevoke(req);
|
return reception.oidcManager.handleRevoke(ctx);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,7 @@
|
|||||||
// Native scope
|
// Native scope
|
||||||
|
import * as crypto from 'node:crypto';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
export { path };
|
export { crypto, path };
|
||||||
|
|
||||||
// Project scope
|
// Project scope
|
||||||
import * as idpInterfaces from '../dist_ts_interfaces/index.js';
|
import * as idpInterfaces from '../dist_ts_interfaces/index.js';
|
||||||
@@ -32,8 +33,10 @@ import * as smartpromise from '@push.rocks/smartpromise';
|
|||||||
import * as smarttime from '@push.rocks/smarttime';
|
import * as smarttime from '@push.rocks/smarttime';
|
||||||
import * as smartunique from '@push.rocks/smartunique';
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||||
|
import * as argon2 from 'argon2';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
argon2,
|
||||||
lik,
|
lik,
|
||||||
projectinfo,
|
projectinfo,
|
||||||
qenv,
|
qenv,
|
||||||
|
|||||||
+38
-16
@@ -1,5 +1,6 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { JwtManager } from './classes.jwtmanager.js';
|
import { JwtManager } from './classes.jwtmanager.js';
|
||||||
|
import type { LoginSession } from './classes.loginsession.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a User is identified by its username or email.
|
* a User is identified by its username or email.
|
||||||
@@ -11,21 +12,27 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
|
|||||||
public static async createJwtForRefreshToken(
|
public static async createJwtForRefreshToken(
|
||||||
jwtManagerInstance: JwtManager,
|
jwtManagerInstance: JwtManager,
|
||||||
refreshTokenArg: string
|
refreshTokenArg: string
|
||||||
) {
|
): Promise<string | null> {
|
||||||
const loginSession =
|
const sessionLookup =
|
||||||
await jwtManagerInstance.receptionRef.loginSessionManager.CLoginSession.getLoginSessionByRefreshToken(
|
await jwtManagerInstance.receptionRef.loginSessionManager.findLoginSessionByRefreshToken(
|
||||||
refreshTokenArg
|
refreshTokenArg
|
||||||
);
|
);
|
||||||
if (!loginSession) {
|
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const refreshTokenValid = await loginSession.validateRefreshToken(refreshTokenArg);
|
|
||||||
if (!refreshTokenValid) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return this.createJwtForLoginSession(jwtManagerInstance, sessionLookup.loginSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createJwtForLoginSession(
|
||||||
|
jwtManagerInstance: JwtManager,
|
||||||
|
loginSession: LoginSession
|
||||||
|
): Promise<string | null> {
|
||||||
const user = await jwtManagerInstance.receptionRef.userManager.CUser.getInstance({
|
const user = await jwtManagerInstance.receptionRef.userManager.CUser.getInstance({
|
||||||
id: loginSession.data.userId,
|
id: loginSession.data.userId,
|
||||||
});
|
});
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const validUntil = plugins.smarttime.ExtendedDate.fromMillis(
|
const validUntil = plugins.smarttime.ExtendedDate.fromMillis(
|
||||||
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 1 })
|
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 1 })
|
||||||
);
|
);
|
||||||
@@ -33,10 +40,10 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
|
|||||||
jwt.id = plugins.smartunique.shortId();
|
jwt.id = plugins.smartunique.shortId();
|
||||||
jwt.data = {
|
jwt.data = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
sessionId: loginSession.id,
|
||||||
validUntil: validUntil.getTime(),
|
validUntil: validUntil.getTime(),
|
||||||
refreshEvery: 1000000,
|
refreshEvery: 1000000,
|
||||||
refreshFrom: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 0.5 }),
|
refreshFrom: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 0.5 }),
|
||||||
refreshToken: await loginSession.getRefreshToken(), // TODO: handle multiple refresh tokens
|
|
||||||
justForLooks: {
|
justForLooks: {
|
||||||
validUntilIsoString: validUntil.toISOString(),
|
validUntilIsoString: validUntil.toISOString(),
|
||||||
}
|
}
|
||||||
@@ -46,7 +53,7 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
|
|||||||
|
|
||||||
const jwtString = await jwtManagerInstance.smartjwtInstance.createJWT({
|
const jwtString = await jwtManagerInstance.smartjwtInstance.createJWT({
|
||||||
id: jwt.id,
|
id: jwt.id,
|
||||||
blocked: null,
|
blocked: false,
|
||||||
data: jwt.data,
|
data: jwt.data,
|
||||||
} as plugins.idpInterfaces.data.IJwt);
|
} as plugins.idpInterfaces.data.IJwt);
|
||||||
return jwtString;
|
return jwtString;
|
||||||
@@ -68,11 +75,26 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getLoginSession() {
|
public async getLoginSession() {
|
||||||
const loginSession = await this.manager.receptionRef.loginSessionManager.CLoginSession.getInstance({
|
if (this.data.sessionId) {
|
||||||
data: {
|
return this.manager.receptionRef.loginSessionManager.CLoginSession.getInstance({
|
||||||
refreshToken: this.data.refreshToken,
|
id: this.data.sessionId,
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
return loginSession;
|
|
||||||
|
if (!this.data.refreshToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionLookup =
|
||||||
|
await this.manager.receptionRef.loginSessionManager.findLoginSessionByRefreshToken(
|
||||||
|
this.data.refreshToken
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sessionLookup) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionLookup.loginSession;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,41 @@ export class JwtManager {
|
|||||||
new plugins.typedrequest.TypedHandler(
|
new plugins.typedrequest.TypedHandler(
|
||||||
'refreshJwt',
|
'refreshJwt',
|
||||||
async (requestArg) => {
|
async (requestArg) => {
|
||||||
const resultJwt = await Jwt.createJwtForRefreshToken(this, requestArg.refreshToken);
|
const sessionLookup =
|
||||||
|
await this.receptionRef.loginSessionManager.findLoginSessionByRefreshToken(
|
||||||
|
requestArg.refreshToken
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sessionLookup || sessionLookup.validationStatus === 'invalid') {
|
||||||
|
return {
|
||||||
|
status: 'not found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionLookup.validationStatus === 'invalidated') {
|
||||||
|
return {
|
||||||
|
status: 'invalidated',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionLookup.validationStatus === 'reused') {
|
||||||
|
await sessionLookup.loginSession.invalidate();
|
||||||
|
return {
|
||||||
|
status: 'invalidated',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotatedRefreshToken = await sessionLookup.loginSession.getRefreshToken();
|
||||||
|
const resultJwt = await Jwt.createJwtForLoginSession(this, sessionLookup.loginSession);
|
||||||
|
if (!rotatedRefreshToken || !resultJwt) {
|
||||||
|
return {
|
||||||
|
status: 'invalidated',
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: 'loggedIn',
|
status: 'loggedIn',
|
||||||
jwt: resultJwt,
|
jwt: resultJwt,
|
||||||
|
refreshToken: rotatedRefreshToken,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -120,19 +151,24 @@ export class JwtManager {
|
|||||||
await this.pushPublicKeyToClients();
|
await this.pushPublicKeyToClients();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async verifyJWTAndGetData(jwtArg: string): Promise<Jwt> {
|
public async verifyJWTAndGetData(jwtArg: string): Promise<Jwt | null> {
|
||||||
const jwtData: plugins.idpInterfaces.data.IJwt = await this.smartjwtInstance.verifyJWTAndGetData(jwtArg);
|
const jwtData: plugins.idpInterfaces.data.IJwt = await this.smartjwtInstance.verifyJWTAndGetData(jwtArg);
|
||||||
const jwt = await this.CJwt.getInstance({
|
const jwt = await this.CJwt.getInstance({
|
||||||
id: jwtData.id,
|
id: jwtData.id,
|
||||||
});
|
});
|
||||||
|
if (!jwt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (jwt.blocked) {
|
if (jwt.blocked) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
const loginSession = await jwt.getLoginSession();
|
const loginSession = await jwt.getLoginSession();
|
||||||
if (!loginSession) {
|
if (!loginSession || loginSession.data.invalidated) {
|
||||||
await jwt.block();
|
await jwt.block();
|
||||||
this.blockedJwtIdList.push(jwt.id);
|
if (!this.blockedJwtIdList.includes(jwt.id)) {
|
||||||
|
this.blockedJwtIdList.push(jwt.id);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import * as plugins from '../plugins.js';
|
|||||||
import { LoginSessionManager } from './classes.loginsessionmanager.js';
|
import { LoginSessionManager } from './classes.loginsessionmanager.js';
|
||||||
import { User } from './classes.user.js';
|
import { User } from './classes.user.js';
|
||||||
|
|
||||||
|
export type TRefreshTokenValidationResult = 'current' | 'invalid' | 'invalidated' | 'reused';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a LoginSession keeps track of a login over the whole time of the user being loggedin
|
* a LoginSession keeps track of a login over the whole time of the user being loggedin
|
||||||
*/
|
*/
|
||||||
@@ -40,7 +42,14 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async getLoginSessionByRefreshToken(refreshTokenArg: string) {
|
public static async getLoginSessionByRefreshToken(refreshTokenArg: string) {
|
||||||
const loginSession = await LoginSession.getInstance({
|
const refreshTokenHash = await LoginSession.hashSessionToken(refreshTokenArg);
|
||||||
|
let loginSession = await LoginSession.getInstance({
|
||||||
|
'data.refreshTokenHash': refreshTokenHash,
|
||||||
|
});
|
||||||
|
if (loginSession) {
|
||||||
|
return loginSession;
|
||||||
|
}
|
||||||
|
loginSession = await LoginSession.getInstance({
|
||||||
data: {
|
data: {
|
||||||
refreshToken: refreshTokenArg,
|
refreshToken: refreshTokenArg,
|
||||||
},
|
},
|
||||||
@@ -48,6 +57,14 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
return loginSession;
|
return loginSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async hashSessionToken(tokenArg: string) {
|
||||||
|
return plugins.smarthash.sha256FromString(tokenArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createOpaqueToken(prefixArg: string) {
|
||||||
|
return `${prefixArg}${plugins.crypto.randomBytes(32).toString('base64url')}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ========
|
// ========
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
// ========
|
// ========
|
||||||
@@ -60,13 +77,17 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ weeks: 1 }),
|
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ weeks: 1 }),
|
||||||
invalidated: false,
|
invalidated: false,
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
|
refreshTokenHash: null,
|
||||||
|
rotatedRefreshTokenHashes: [],
|
||||||
|
transferTokenHash: null,
|
||||||
|
transferTokenExpiresAt: null,
|
||||||
deviceId: null,
|
deviceId: null,
|
||||||
deviceInfo: null,
|
deviceInfo: null,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
lastActive: Date.now(),
|
lastActive: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
public transferToken: string;
|
public transferToken: string | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -77,40 +98,99 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
*/
|
*/
|
||||||
public async invalidate() {
|
public async invalidate() {
|
||||||
this.data.invalidated = true;
|
this.data.invalidated = true;
|
||||||
|
this.data.refreshToken = null;
|
||||||
|
this.data.refreshTokenHash = null;
|
||||||
|
this.data.transferTokenHash = null;
|
||||||
|
this.data.transferTokenExpiresAt = null;
|
||||||
await this.save();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a refresh token is unique to a login session and ONLY created once per login session
|
* a refresh token is unique to a login session and rotated whenever it is issued
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public async getRefreshToken() {
|
public async getRefreshToken() {
|
||||||
if (this.data.invalidated) {
|
if (this.data.invalidated) {
|
||||||
console.log('login session is invalidated. no refresh token can be generated.');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!this.data.refreshToken) {
|
const previousRefreshTokenHash =
|
||||||
this.data.refreshToken = plugins.smartunique.uni('refresh_');
|
this.data.refreshTokenHash ||
|
||||||
|
(this.data.refreshToken
|
||||||
|
? await LoginSession.hashSessionToken(this.data.refreshToken)
|
||||||
|
: null);
|
||||||
|
|
||||||
|
if (previousRefreshTokenHash) {
|
||||||
|
this.data.rotatedRefreshTokenHashes = [
|
||||||
|
...(this.data.rotatedRefreshTokenHashes || []),
|
||||||
|
previousRefreshTokenHash,
|
||||||
|
].slice(-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshToken = LoginSession.createOpaqueToken('refresh_');
|
||||||
|
this.data.refreshTokenHash = await LoginSession.hashSessionToken(refreshToken);
|
||||||
|
this.data.refreshToken = null;
|
||||||
|
this.data.lastActive = Date.now();
|
||||||
await this.save();
|
await this.save();
|
||||||
return this.data.refreshToken;
|
return refreshToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTransferToken() {
|
public async getTransferToken() {
|
||||||
this.transferToken = plugins.smartunique.uni('transfer_');
|
this.transferToken = LoginSession.createOpaqueToken('transfer_');
|
||||||
|
this.data.transferTokenHash = await LoginSession.hashSessionToken(this.transferToken);
|
||||||
|
this.data.transferTokenExpiresAt =
|
||||||
|
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ minutes: 5 });
|
||||||
|
await this.save();
|
||||||
return this.transferToken;
|
return this.transferToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async validateRefreshToken(refreshTokenArg: string) {
|
public async validateRefreshToken(
|
||||||
return this.data.refreshToken === refreshTokenArg;
|
refreshTokenArg: string
|
||||||
|
): Promise<TRefreshTokenValidationResult> {
|
||||||
|
if (this.data.invalidated) {
|
||||||
|
return 'invalidated';
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTokenHash = await LoginSession.hashSessionToken(refreshTokenArg);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.data.refreshTokenHash === refreshTokenHash ||
|
||||||
|
(!!this.data.refreshToken && this.data.refreshToken === refreshTokenArg)
|
||||||
|
) {
|
||||||
|
return 'current';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.data.rotatedRefreshTokenHashes || []).includes(refreshTokenHash)) {
|
||||||
|
return 'reused';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'invalid';
|
||||||
}
|
}
|
||||||
|
|
||||||
public async validateTransferToken(transferTokenArg: string) {
|
public async validateTransferToken(transferTokenArg: string) {
|
||||||
const result = this.transferToken === transferTokenArg;
|
if (this.data.invalidated || !this.data.transferTokenHash) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.data.transferTokenExpiresAt &&
|
||||||
|
this.data.transferTokenExpiresAt < Date.now()
|
||||||
|
) {
|
||||||
|
this.data.transferTokenHash = null;
|
||||||
|
this.data.transferTokenExpiresAt = null;
|
||||||
|
await this.save();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result =
|
||||||
|
this.data.transferTokenHash ===
|
||||||
|
(await LoginSession.hashSessionToken(transferTokenArg));
|
||||||
|
|
||||||
// a transfer token can only be used once, so we invalidate it here
|
// a transfer token can only be used once, so we invalidate it here
|
||||||
if (result) {
|
if (result) {
|
||||||
this.transferToken = null;
|
this.transferToken = null;
|
||||||
|
this.data.transferTokenHash = null;
|
||||||
|
this.data.transferTokenExpiresAt = null;
|
||||||
|
await this.save();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { LoginSession } from './classes.loginsession.js';
|
import { LoginSession, type TRefreshTokenValidationResult } from './classes.loginsession.js';
|
||||||
import { Reception } from './classes.reception.js';
|
import { Reception } from './classes.reception.js';
|
||||||
import { logger } from './logging.js';
|
import { logger } from './logging.js';
|
||||||
|
|
||||||
@@ -32,9 +32,6 @@ export class LoginSessionManager {
|
|||||||
let user = await this.receptionRef.userManager.CUser.getInstance({
|
let user = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
data: {
|
data: {
|
||||||
username: requestData.username,
|
username: requestData.username,
|
||||||
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
|
|
||||||
requestData.password
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,33 +39,30 @@ export class LoginSessionManager {
|
|||||||
user = await this.receptionRef.userManager.CUser.getInstance({
|
user = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
data: {
|
data: {
|
||||||
email: requestData.username,
|
email: requestData.username,
|
||||||
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
|
|
||||||
requestData.password
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
if (user && (await this.receptionRef.userManager.CUser.verifyPassword(
|
||||||
// lets recheck
|
requestData.password,
|
||||||
if (
|
user.data.passwordHash
|
||||||
(user.data.username !== requestData.username &&
|
))) {
|
||||||
user.data.email !== requestData.username) ||
|
if (this.receptionRef.userManager.CUser.shouldUpgradePasswordHash(user.data.passwordHash)) {
|
||||||
user.data.passwordHash !==
|
user.data.passwordHash = await this.receptionRef.userManager.CUser.hashPassword(
|
||||||
(await this.receptionRef.userManager.CUser.hashPassword(requestData.password))
|
requestData.password
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'database returned a user that does not match wanted criterea. CRITICAL!'
|
|
||||||
);
|
);
|
||||||
|
await user.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||||
this.loginSessions.add(loginSession);
|
this.loginSessions.add(loginSession);
|
||||||
const refreshToken = await loginSession.getRefreshToken();
|
const refreshToken = await loginSession.getRefreshToken();
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
refreshToken,
|
||||||
refreshToken: refreshToken,
|
|
||||||
twoFaNeeded: false,
|
twoFaNeeded: false,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -109,12 +103,14 @@ export class LoginSessionManager {
|
|||||||
} else {
|
} else {
|
||||||
logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`);
|
logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`);
|
||||||
}
|
}
|
||||||
|
const testOnlyToken =
|
||||||
|
process.env.TEST_MODE && existingUser
|
||||||
|
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
|
||||||
|
?.token
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
testOnlyToken: process.env.TEST_MODE
|
testOnlyToken,
|
||||||
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
|
|
||||||
.token
|
|
||||||
: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -133,10 +129,17 @@ export class LoginSessionManager {
|
|||||||
email: requestArg.email,
|
email: requestArg.email,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||||
|
}
|
||||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||||
this.loginSessions.add(loginSession);
|
this.loginSessions.add(loginSession);
|
||||||
|
const refreshToken = await loginSession.getRefreshToken();
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
refreshToken: await loginSession.getRefreshToken(),
|
refreshToken,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new plugins.typedrequest.TypedResponseError('Validation Token not found');
|
throw new plugins.typedrequest.TypedResponseError('Validation Token not found');
|
||||||
@@ -147,8 +150,11 @@ export class LoginSessionManager {
|
|||||||
|
|
||||||
this.typedRouter.addTypedHandler<plugins.idpInterfaces.request.ILogoutRequest>(
|
this.typedRouter.addTypedHandler<plugins.idpInterfaces.request.ILogoutRequest>(
|
||||||
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
|
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
|
||||||
const loginSession = await this.CLoginSession.getLoginSessionByRefreshToken(requestDataArg.refreshToken);
|
const sessionLookup = await this.findLoginSessionByRefreshToken(requestDataArg.refreshToken);
|
||||||
await loginSession.invalidate();
|
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Invalid refresh token');
|
||||||
|
}
|
||||||
|
await sessionLookup.loginSession.invalidate();
|
||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -158,31 +164,39 @@ export class LoginSessionManager {
|
|||||||
'exchangeRefreshTokenAndTransferToken',
|
'exchangeRefreshTokenAndTransferToken',
|
||||||
async (requestDataArg) => {
|
async (requestDataArg) => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case !!requestDataArg.refreshToken:
|
case !!requestDataArg.refreshToken: {
|
||||||
const loginSession = await this.loginSessions.find(async (loginSessionArg) => {
|
const sessionLookup = await this.findLoginSessionByRefreshToken(
|
||||||
return loginSessionArg.validateRefreshToken(requestDataArg.refreshToken);
|
requestDataArg.refreshToken
|
||||||
});
|
);
|
||||||
if (!loginSession) {
|
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
|
||||||
|
if (sessionLookup?.validationStatus === 'reused') {
|
||||||
|
await sessionLookup.loginSession.invalidate();
|
||||||
|
}
|
||||||
throw new plugins.typedrequest.TypedResponseError('your refresh token is invalid');
|
throw new plugins.typedrequest.TypedResponseError('your refresh token is invalid');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
transferToken: await loginSession.getTransferToken(),
|
transferToken: await sessionLookup.loginSession.getTransferToken(),
|
||||||
};
|
};
|
||||||
break;
|
}
|
||||||
case !!requestDataArg.transferToken:
|
case !!requestDataArg.transferToken: {
|
||||||
let transferToken: string;
|
const loginSession2 = await this.findLoginSessionByTransferToken(
|
||||||
const loginSession2 = await this.loginSessions.find(async (loginSessionArg) => {
|
requestDataArg.transferToken
|
||||||
return loginSessionArg.validateTransferToken(requestDataArg.transferToken);
|
);
|
||||||
});
|
|
||||||
if (!loginSession2) {
|
if (!loginSession2) {
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'Your transfer token is not valid.'
|
'Your transfer token is not valid.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const refreshToken = await loginSession2.getRefreshToken();
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
refreshToken: await loginSession2.getRefreshToken(),
|
refreshToken,
|
||||||
};
|
};
|
||||||
break;
|
}
|
||||||
|
default:
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Invalid token exchange request');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -271,8 +285,7 @@ export class LoginSessionManager {
|
|||||||
throw new plugins.typedrequest.TypedResponseError('Invalid JWT');
|
throw new plugins.typedrequest.TypedResponseError('Invalid JWT');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current session's refresh token to identify the current session
|
const currentLoginSession = await jwt.getLoginSession();
|
||||||
const currentRefreshToken = jwt.data.refreshToken;
|
|
||||||
|
|
||||||
// Get all sessions for this user
|
// Get all sessions for this user
|
||||||
const sessions = await this.CLoginSession.getInstances({
|
const sessions = await this.CLoginSession.getInstances({
|
||||||
@@ -290,7 +303,7 @@ export class LoginSessionManager {
|
|||||||
ip: session.data.deviceInfo?.ip || 'Unknown',
|
ip: session.data.deviceInfo?.ip || 'Unknown',
|
||||||
lastActive: session.data.lastActive || session.data.createdAt || Date.now(),
|
lastActive: session.data.lastActive || session.data.createdAt || Date.now(),
|
||||||
createdAt: session.data.createdAt || Date.now(),
|
createdAt: session.data.createdAt || Date.now(),
|
||||||
isCurrent: session.data.refreshToken === currentRefreshToken,
|
isCurrent: session.id === currentLoginSession?.id,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -317,8 +330,10 @@ export class LoginSessionManager {
|
|||||||
throw new plugins.typedrequest.TypedResponseError('Session not found');
|
throw new plugins.typedrequest.TypedResponseError('Session not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentLoginSession = await jwt.getLoginSession();
|
||||||
|
|
||||||
// Don't allow revoking the current session via this method
|
// Don't allow revoking the current session via this method
|
||||||
if (sessionToRevoke.data.refreshToken === jwt.data.refreshToken) {
|
if (sessionToRevoke.id === currentLoginSession?.id) {
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'Cannot revoke current session. Use logout instead.'
|
'Cannot revoke current session. Use logout instead.'
|
||||||
);
|
);
|
||||||
@@ -338,4 +353,44 @@ export class LoginSessionManager {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async findLoginSessionByRefreshToken(refreshTokenArg: string): Promise<{
|
||||||
|
loginSession: LoginSession;
|
||||||
|
validationStatus: TRefreshTokenValidationResult;
|
||||||
|
} | null> {
|
||||||
|
const directMatch = await this.CLoginSession.getLoginSessionByRefreshToken(refreshTokenArg);
|
||||||
|
if (directMatch) {
|
||||||
|
return {
|
||||||
|
loginSession: directMatch,
|
||||||
|
validationStatus: await directMatch.validateRefreshToken(refreshTokenArg),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSessions = await this.CLoginSession.getInstances({});
|
||||||
|
for (const loginSession of loginSessions) {
|
||||||
|
const validationStatus = await loginSession.validateRefreshToken(refreshTokenArg);
|
||||||
|
if (validationStatus !== 'invalid') {
|
||||||
|
return {
|
||||||
|
loginSession,
|
||||||
|
validationStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findLoginSessionByTransferToken(transferTokenArg: string) {
|
||||||
|
const transferTokenHash = await LoginSession.hashSessionToken(transferTokenArg);
|
||||||
|
const loginSession = await this.CLoginSession.getInstance({
|
||||||
|
'data.transferTokenHash': transferTokenHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!loginSession) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await loginSession.validateTransferToken(transferTokenArg);
|
||||||
|
return isValid ? loginSession : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,9 +95,8 @@ export class OidcManager {
|
|||||||
/**
|
/**
|
||||||
* Handle the authorization endpoint request
|
* Handle the authorization endpoint request
|
||||||
*/
|
*/
|
||||||
public async handleAuthorize(request: Request): Promise<Response> {
|
public async handleAuthorize(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
|
||||||
const url = new URL(request.url);
|
const params = ctx.url.searchParams;
|
||||||
const params = url.searchParams;
|
|
||||||
|
|
||||||
// Extract authorization request parameters
|
// Extract authorization request parameters
|
||||||
const clientId = params.get('client_id');
|
const clientId = params.get('client_id');
|
||||||
@@ -196,21 +195,21 @@ export class OidcManager {
|
|||||||
/**
|
/**
|
||||||
* Handle the token endpoint request
|
* Handle the token endpoint request
|
||||||
*/
|
*/
|
||||||
public async handleToken(request: Request): Promise<Response> {
|
public async handleToken(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
|
||||||
// Parse form data
|
// Parse form data
|
||||||
const contentType = request.headers.get('content-type');
|
const contentType = ctx.headers.get('content-type');
|
||||||
if (!contentType?.includes('application/x-www-form-urlencoded')) {
|
if (!contentType?.includes('application/x-www-form-urlencoded')) {
|
||||||
return this.tokenErrorResponse('invalid_request', 'Content-Type must be application/x-www-form-urlencoded');
|
return this.tokenErrorResponse('invalid_request', 'Content-Type must be application/x-www-form-urlencoded');
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await ctx.formData();
|
||||||
const grantType = formData.get('grant_type') as string;
|
const grantType = formData.get('grant_type') as string;
|
||||||
|
|
||||||
// Extract client credentials from Basic auth or form
|
// Extract client credentials from Basic auth or form
|
||||||
let clientId = formData.get('client_id') as string;
|
let clientId = formData.get('client_id') as string;
|
||||||
let clientSecret = formData.get('client_secret') as string;
|
let clientSecret = formData.get('client_secret') as string;
|
||||||
|
|
||||||
const authHeader = request.headers.get('authorization');
|
const authHeader = ctx.headers.get('authorization');
|
||||||
if (authHeader?.startsWith('Basic ')) {
|
if (authHeader?.startsWith('Basic ')) {
|
||||||
const base64 = authHeader.substring(6);
|
const base64 = authHeader.substring(6);
|
||||||
const decoded = Buffer.from(base64, 'base64').toString('utf-8');
|
const decoded = Buffer.from(base64, 'base64').toString('utf-8');
|
||||||
@@ -469,9 +468,9 @@ export class OidcManager {
|
|||||||
/**
|
/**
|
||||||
* Handle the userinfo endpoint
|
* Handle the userinfo endpoint
|
||||||
*/
|
*/
|
||||||
public async handleUserInfo(request: Request): Promise<Response> {
|
public async handleUserInfo(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
|
||||||
// Get access token from Authorization header
|
// Get access token from Authorization header
|
||||||
const authHeader = request.headers.get('authorization');
|
const authHeader = ctx.headers.get('authorization');
|
||||||
if (!authHeader?.startsWith('Bearer ')) {
|
if (!authHeader?.startsWith('Bearer ')) {
|
||||||
return new Response(JSON.stringify({ error: 'invalid_token' }), {
|
return new Response(JSON.stringify({ error: 'invalid_token' }), {
|
||||||
status: 401,
|
status: 401,
|
||||||
@@ -575,8 +574,8 @@ export class OidcManager {
|
|||||||
/**
|
/**
|
||||||
* Handle the revocation endpoint
|
* Handle the revocation endpoint
|
||||||
*/
|
*/
|
||||||
public async handleRevoke(request: Request): Promise<Response> {
|
public async handleRevoke(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
|
||||||
const formData = await request.formData();
|
const formData = await ctx.formData();
|
||||||
const token = formData.get('token') as string;
|
const token = formData.get('token') as string;
|
||||||
const tokenTypeHint = formData.get('token_type_hint') as string;
|
const tokenTypeHint = formData.get('token_type_hint') as string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import { logger } from './logging.js';
|
import { logger } from './logging.js';
|
||||||
|
|
||||||
import { JwtManager } from './classes.jwtmanager.js';
|
import { JwtManager } from './classes.jwtmanager.js';
|
||||||
@@ -30,7 +29,6 @@ export interface IReceptionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Reception {
|
export class Reception {
|
||||||
public projectinfoNpm = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
public serviceQenv = new plugins.qenv.Qenv('./', './.nogit');
|
public serviceQenv = new plugins.qenv.Qenv('./', './.nogit');
|
||||||
public szPlatformClient = new plugins.szPlatformClient.SzPlatformClient();
|
public szPlatformClient = new plugins.szPlatformClient.SzPlatformClient();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export class ReceptionDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
console.log(this.receptionRef.options.mongoDescriptor);
|
|
||||||
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.receptionRef.options.mongoDescriptor);
|
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.receptionRef.options.mongoDescriptor);
|
||||||
await this.smartdataDb.init();
|
await this.smartdataDb.init();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
const newUser = new User();
|
const newUser = new User();
|
||||||
newUser.id = plugins.smartunique.shortId();
|
newUser.id = plugins.smartunique.shortId();
|
||||||
newUser.data = {
|
newUser.data = {
|
||||||
connectedOrgs: null,
|
connectedOrgs: [],
|
||||||
status: 'new',
|
status: 'new',
|
||||||
name: userDataArg.name,
|
name: userDataArg.name,
|
||||||
username: userDataArg.username,
|
username: userDataArg.username,
|
||||||
@@ -31,8 +31,26 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static hashPassword(passwordArg: string) {
|
public static async hashPassword(passwordArg: string) {
|
||||||
return plugins.smarthash.sha256FromString(passwordArg);
|
return plugins.argon2.hash(passwordArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isLegacyPasswordHash(passwordHashArg?: string) {
|
||||||
|
return !!passwordHashArg && !passwordHashArg.startsWith('$argon2');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static shouldUpgradePasswordHash(passwordHashArg?: string) {
|
||||||
|
return this.isLegacyPasswordHash(passwordHashArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async verifyPassword(passwordArg: string, passwordHashArg?: string) {
|
||||||
|
if (!passwordHashArg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.isLegacyPasswordHash(passwordHashArg)) {
|
||||||
|
return passwordHashArg === (await plugins.smarthash.sha256FromString(passwordArg));
|
||||||
|
}
|
||||||
|
return plugins.argon2.verify(passwordHashArg, passwordArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export class UserManager {
|
|||||||
new plugins.typedrequest.TypedHandler('getRolesAndOrganizationsForUserId', async reqArg => {
|
new plugins.typedrequest.TypedHandler('getRolesAndOrganizationsForUserId', async reqArg => {
|
||||||
console.log('user manager: getting roles and orgs');
|
console.log('user manager: getting roles and orgs');
|
||||||
const user = await this.getUserByJwtValidation(reqArg.jwt);
|
const user = await this.getUserByJwtValidation(reqArg.jwt);
|
||||||
|
if (!user) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||||
|
}
|
||||||
const organizations = await this.receptionRef.organizationmanager.getAllOrganizationsForUser(
|
const organizations = await this.receptionRef.organizationmanager.getAllOrganizationsForUser(
|
||||||
user
|
user
|
||||||
);
|
);
|
||||||
@@ -49,8 +52,7 @@ export class UserManager {
|
|||||||
email: user.data.email,
|
email: user.data.email,
|
||||||
mobileNumber: user.data.mobileNumber,
|
mobileNumber: user.data.mobileNumber,
|
||||||
connectedOrgs: user.data.connectedOrgs,
|
connectedOrgs: user.data.connectedOrgs,
|
||||||
status: null,
|
status: user.data.status,
|
||||||
password: null,
|
|
||||||
isGlobalAdmin: user.data.isGlobalAdmin,
|
isGlobalAdmin: user.data.isGlobalAdmin,
|
||||||
} as plugins.idpInterfaces.data.IUser['data']
|
} as plugins.idpInterfaces.data.IUser['data']
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,9 @@ export class UserManager {
|
|||||||
*/
|
*/
|
||||||
public async getUserByJwt(jwtString: string) {
|
public async getUserByJwt(jwtString: string) {
|
||||||
const jwtInstance = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtString);
|
const jwtInstance = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtString);
|
||||||
|
if (!jwtInstance) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const user = await this.CUser.getInstance({
|
const user = await this.CUser.getInstance({
|
||||||
id: jwtInstance.data.userId
|
id: jwtInstance.data.userId
|
||||||
});
|
});
|
||||||
@@ -75,7 +80,10 @@ export class UserManager {
|
|||||||
* faster than the "getUserByJwt"
|
* faster than the "getUserByJwt"
|
||||||
*/
|
*/
|
||||||
public async getUserByJwtValidation(jwtStringArg: string) {
|
public async getUserByJwtValidation(jwtStringArg: string) {
|
||||||
const jwtDataArg: plugins.idpInterfaces.data.IJwt = await this.receptionRef.jwtManager.smartjwtInstance.verifyJWTAndGetData(jwtStringArg);
|
const jwtDataArg = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtStringArg);
|
||||||
|
if (!jwtDataArg) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const resultingUser = await this.CUser.getInstance({
|
const resultingUser = await this.CUser.getInstance({
|
||||||
id: jwtDataArg.data.userId
|
id: jwtDataArg.data.userId
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as paths from '../paths.js';
|
|
||||||
|
|
||||||
const projectinfoNpm = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
|
||||||
|
|
||||||
export const logger = new plugins.smartlog.ConsoleLog();
|
export const logger = new plugins.smartlog.ConsoleLog();
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ export class IdpCli {
|
|||||||
this.storeCredentials({
|
this.storeCredentials({
|
||||||
...credentials,
|
...credentials,
|
||||||
jwt: response.jwt,
|
jwt: response.jwt,
|
||||||
|
refreshToken: response.refreshToken || credentials.refreshToken,
|
||||||
});
|
});
|
||||||
return response.jwt;
|
return response.jwt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
# @idp.global/cli
|
||||||
|
|
||||||
|
Command-line interface for interacting with the idp.global Identity Provider. A Node.js CLI tool that provides authentication, user management, and organization administration from the terminal.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The IdpCli module provides a complete command-line interface for managing your idp.global account and organizations. It uses file-based credential storage and WebSocket connections for real-time communication with the IdP server.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @idp.global/cli
|
||||||
|
# or
|
||||||
|
pnpm add -g @idp.global/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login with email and password
|
||||||
|
idp login
|
||||||
|
|
||||||
|
# Check current user
|
||||||
|
idp whoami
|
||||||
|
|
||||||
|
# List your organizations
|
||||||
|
idp orgs
|
||||||
|
|
||||||
|
# Logout
|
||||||
|
idp logout
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `idp login` | Interactive login with email and password |
|
||||||
|
| `idp login-token` | Login with an API token |
|
||||||
|
| `idp logout` | Clear stored credentials and end session |
|
||||||
|
|
||||||
|
### User Information
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `idp whoami` | Display current user information |
|
||||||
|
| `idp sessions` | List all active sessions |
|
||||||
|
| `idp revoke --session <id>` | Revoke a specific session |
|
||||||
|
|
||||||
|
### Organization Management
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `idp orgs` | List all organizations you belong to |
|
||||||
|
| `idp orgs-create` | Create a new organization (interactive) |
|
||||||
|
| `idp members --org <id>` | List members of an organization |
|
||||||
|
| `idp invite --org <id> --email <email>` | Invite a user to an organization |
|
||||||
|
|
||||||
|
### Admin Commands (Global Admins Only)
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `idp admin-check` | Check if you are a global admin |
|
||||||
|
| `idp admin-apps` | List all global apps with connection stats |
|
||||||
|
| `idp admin-suspend --user <id>` | Suspend a user account |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `IDP_URL` | Override the IdP server URL | `https://idp.global` |
|
||||||
|
|
||||||
|
### Credential Storage
|
||||||
|
|
||||||
|
Credentials are stored in `~/.idp-global/credentials.json`. This file contains your refresh token and JWT for persistent authentication across CLI sessions.
|
||||||
|
|
||||||
|
## Programmatic Usage
|
||||||
|
|
||||||
|
You can also use the IdpCli class programmatically:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IdpCli } from '@idp.global/cli';
|
||||||
|
|
||||||
|
const cli = new IdpCli({
|
||||||
|
idpBaseUrl: 'https://idp.global',
|
||||||
|
configDir: '/custom/config/path', // optional
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login
|
||||||
|
await cli.loginWithPassword('user@example.com', 'password');
|
||||||
|
|
||||||
|
// Get current user
|
||||||
|
const user = await cli.whoami();
|
||||||
|
console.log('Logged in as:', user.data.name);
|
||||||
|
|
||||||
|
// Get organizations
|
||||||
|
const { organizations, roles } = await cli.getOrganizations();
|
||||||
|
for (const org of organizations) {
|
||||||
|
console.log(`- ${org.data.name} (${org.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect when done
|
||||||
|
await cli.disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
### IdpCli Class Methods
|
||||||
|
|
||||||
|
**Authentication:**
|
||||||
|
- `loginWithPassword(email, password)` - Login with credentials
|
||||||
|
- `loginWithApiToken(token)` - Login with API token
|
||||||
|
- `refreshJwt()` - Refresh the current JWT
|
||||||
|
- `logout()` - Clear credentials and end session
|
||||||
|
|
||||||
|
**User:**
|
||||||
|
- `whoami()` - Get current user info
|
||||||
|
- `getSessions()` - Get active sessions
|
||||||
|
- `revokeSession(sessionId)` - Revoke a session
|
||||||
|
|
||||||
|
**Organizations:**
|
||||||
|
- `getOrganizations()` - List user's organizations
|
||||||
|
- `createOrganization(name, slug, mode)` - Create new organization
|
||||||
|
- `getOrgMembers(orgId)` - Get organization members
|
||||||
|
- `inviteMember(orgId, email, roles)` - Invite a user
|
||||||
|
|
||||||
|
**Admin:**
|
||||||
|
- `checkGlobalAdmin()` - Check admin status
|
||||||
|
- `getGlobalAppStats()` - Get app statistics
|
||||||
|
- `suspendUser(userId)` - Suspend a user
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Create an Organization
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ idp orgs-create
|
||||||
|
Organization Name: My Company
|
||||||
|
Organization Slug: my-company
|
||||||
|
|
||||||
|
Organization created successfully!
|
||||||
|
ID: org_abc123
|
||||||
|
Name: My Company
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invite Team Members
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ idp invite --org org_abc123 --email colleague@example.com
|
||||||
|
Invitation sent to colleague@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Active Sessions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ idp sessions
|
||||||
|
|
||||||
|
Active Sessions:
|
||||||
|
- sess_xyz789
|
||||||
|
Device: MacBook Pro
|
||||||
|
Browser: Chrome
|
||||||
|
OS: macOS
|
||||||
|
Last Active: 1/29/2025, 2:30:00 PM
|
||||||
|
Current: Yes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `@api.global/typedrequest` - Type-safe API requests
|
||||||
|
- `@api.global/typedsocket` - WebSocket communication
|
||||||
|
- `@push.rocks/smartcli` - CLI framework
|
||||||
|
- `@push.rocks/smartinteract` - Interactive prompts
|
||||||
|
- `@idp.global/interfaces` - TypeScript interfaces
|
||||||
|
|
||||||
|
## License and Legal Information
|
||||||
|
|
||||||
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
@@ -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}`);
|
||||||
|
|||||||
+19
-2
@@ -367,6 +367,23 @@ Access via `idpClient.requests.*`:
|
|||||||
|
|
||||||
**Admin**: `checkGlobalAdmin`, `getGlobalAppStats`, `createGlobalApp`, `updateGlobalApp`, `deleteGlobalApp`, `suspendUser`, `deleteSuspendedUser`
|
**Admin**: `checkGlobalAdmin`, `getGlobalAppStats`, `createGlobalApp`, `updateGlobalApp`, `deleteGlobalApp`, `suspendUser`, `deleteSuspendedUser`
|
||||||
|
|
||||||
## License
|
## License and Legal Information
|
||||||
|
|
||||||
MIT - See the main repository for full license details.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
+19
-2
@@ -307,6 +307,23 @@ interface IReq_LoginWithEmailOrUsernameAndPassword {
|
|||||||
| `organizations` | User's organization memberships |
|
| `organizations` | User's organization memberships |
|
||||||
| `roles` | User's roles within organizations |
|
| `roles` | User's roles within organizations |
|
||||||
|
|
||||||
## License
|
## License and Legal Information
|
||||||
|
|
||||||
MIT - See the main repository for full license details.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ export interface IReq_RefreshJwt
|
|||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
status: data.TLoginStatus;
|
status: data.TLoginStatus;
|
||||||
jwt: string;
|
jwt?: string;
|
||||||
|
refreshToken?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@idp.global/idp.global',
|
name: '@idp.global/idp.global',
|
||||||
version: '1.14.0',
|
version: '1.17.0',
|
||||||
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { accountDesignTokens } from './sharedstyles.js';
|
|||||||
import * as views from './views/index.js';
|
import * as views from './views/index.js';
|
||||||
import * as accountstate from '../../states/accountstate.js';
|
import * as accountstate from '../../states/accountstate.js';
|
||||||
|
|
||||||
import { commitinfo } from '../../../dist_ts/00_commitinfo_data.js';
|
import { commitinfo } from '../../../ts/00_commitinfo_data.js';
|
||||||
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { accountDesignTokens } from './sharedstyles.js';
|
|||||||
import { CreateOrgModal } from './create-org-modal.js';
|
import { CreateOrgModal } from './create-org-modal.js';
|
||||||
import { OrgSelectModal } from './org-select-modal.js';
|
import { OrgSelectModal } from './org-select-modal.js';
|
||||||
|
|
||||||
import { commitinfo } from '../../../dist_ts/00_commitinfo_data.js';
|
import { commitinfo } from '../../../ts/00_commitinfo_data.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
|
|||||||
@@ -30,6 +30,19 @@ export const cardStyles = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base styles for all view components
|
||||||
|
* Provides consistent background and foreground colors
|
||||||
|
*/
|
||||||
|
export const viewBaseStyles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
min-height: 100%;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Typography styles for consistent text hierarchy
|
* Typography styles for consistent text hierarchy
|
||||||
*/
|
*/
|
||||||
@@ -108,10 +121,3 @@ export const navigationStyles = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy export for backwards compatibility
|
|
||||||
*/
|
|
||||||
export default css`
|
|
||||||
${accountDesignTokens}
|
|
||||||
${typographyStyles}
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import { IdpState } from '../../../states/idp.state.js';
|
import { IdpState } from '../../../states/idp.state.js';
|
||||||
import { accountDesignTokens } from '../sharedstyles.js';
|
import * as sharedStyles from '../sharedstyles.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -43,15 +43,9 @@ export class AdminView extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
accountDesignTokens,
|
sharedStyles.accountDesignTokens,
|
||||||
|
sharedStyles.viewBaseStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
min-height: 100%;
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
state,
|
state,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js';
|
import * as sharedStyles from '../sharedstyles.js';
|
||||||
import * as accountState from '../../../states/accountstate.js';
|
import * as accountState from '../../../states/accountstate.js';
|
||||||
import { IdpState } from '../../../states/idp.state.js';
|
import { IdpState } from '../../../states/idp.state.js';
|
||||||
|
|
||||||
@@ -45,12 +45,12 @@ export class AppsView extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
accountDesignTokens,
|
sharedStyles.accountDesignTokens,
|
||||||
cardStyles,
|
sharedStyles.viewBaseStyles,
|
||||||
typographyStyles,
|
sharedStyles.cardStyles,
|
||||||
|
sharedStyles.typographyStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
|
||||||
padding: 48px;
|
padding: 48px;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import { accountDesignTokens } from '../sharedstyles.js';
|
import * as sharedStyles from '../sharedstyles.js';
|
||||||
import * as accountStateModule from '../../../states/accountstate.js';
|
import * as accountStateModule from '../../../states/accountstate.js';
|
||||||
import { IdpState } from '../../../states/idp.state.js';
|
import { IdpState } from '../../../states/idp.state.js';
|
||||||
|
|
||||||
@@ -59,15 +59,9 @@ export class BaseView extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
accountDesignTokens,
|
sharedStyles.accountDesignTokens,
|
||||||
|
sharedStyles.viewBaseStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
min-height: 100%;
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import { accountDesignTokens } from '../sharedstyles.js';
|
import * as sharedStyles from '../sharedstyles.js';
|
||||||
import * as accountStateModule from '../../../states/accountstate.js';
|
import * as accountStateModule from '../../../states/accountstate.js';
|
||||||
import { IdpState } from '../../../states/idp.state.js';
|
import { IdpState } from '../../../states/idp.state.js';
|
||||||
|
|
||||||
@@ -41,14 +41,9 @@ export class OrgView extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
accountDesignTokens,
|
sharedStyles.accountDesignTokens,
|
||||||
|
sharedStyles.viewBaseStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
min-height: 100%;
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../../plugins.js';
|
||||||
import sharedStyles from '../sharedstyles.js';
|
import * as sharedStyles from '../sharedstyles.js';
|
||||||
import * as state from '../../../states/accountstate.js';
|
import * as state from '../../../states/accountstate.js';
|
||||||
import { IdpState } from '../../../states/idp.state.js';
|
import { IdpState } from '../../../states/idp.state.js';
|
||||||
|
|
||||||
@@ -23,13 +23,13 @@ declare global {
|
|||||||
export class PaddleSetupView extends DeesElement {
|
export class PaddleSetupView extends DeesElement {
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
sharedStyles,
|
sharedStyles.accountDesignTokens,
|
||||||
|
sharedStyles.viewBaseStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
padding: 48px;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: auto;
|
margin: 0 auto;
|
||||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js';
|
import * as sharedStyles from '../sharedstyles.js';
|
||||||
|
|
||||||
import * as state from '../../../states/accountstate.js';
|
import * as state from '../../../states/accountstate.js';
|
||||||
|
|
||||||
@@ -46,12 +46,12 @@ export class SubscriptionView extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
accountDesignTokens,
|
sharedStyles.accountDesignTokens,
|
||||||
cardStyles,
|
sharedStyles.viewBaseStyles,
|
||||||
typographyStyles,
|
sharedStyles.cardStyles,
|
||||||
|
sharedStyles.typographyStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
|
||||||
padding: 48px;
|
padding: 48px;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js';
|
import * as sharedStyles from '../sharedstyles.js';
|
||||||
import * as accountState from '../../../states/accountstate.js';
|
import * as accountState from '../../../states/accountstate.js';
|
||||||
import { IdpState } from '../../../states/idp.state.js';
|
import { IdpState } from '../../../states/idp.state.js';
|
||||||
import { BulkInviteModal } from '../bulk-invite-modal.js';
|
import { BulkInviteModal } from '../bulk-invite-modal.js';
|
||||||
@@ -83,12 +83,12 @@ export class UsersView extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
accountDesignTokens,
|
sharedStyles.accountDesignTokens,
|
||||||
cardStyles,
|
sharedStyles.viewBaseStyles,
|
||||||
typographyStyles,
|
sharedStyles.cardStyles,
|
||||||
|
sharedStyles.typographyStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
|
||||||
padding: 48px;
|
padding: 48px;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
query,
|
query,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import { commitinfo } from '../../dist_ts/00_commitinfo_data.js';
|
import { commitinfo } from '../../ts/00_commitinfo_data.js';
|
||||||
import { IdpState } from '../states/idp.state.js';
|
import { IdpState } from '../states/idp.state.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -207,21 +207,14 @@ export class IdpRegistrationPrompt extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handleRefreshToken(refreshTokenArg: string, delayDispatchMillisArg = 0) {
|
public async handleRefreshToken(refreshTokenArg: string, delayDispatchMillisArg = 0) {
|
||||||
// a refreshToken binds directly to a session.
|
|
||||||
// the refresh token is used on a continuous basis to get fresh and short-lived jwts
|
|
||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const refreshJwt = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
|
const jwt = await idpState.idpClient.refreshJwt(refreshTokenArg);
|
||||||
'refreshJwt'
|
|
||||||
);
|
|
||||||
const responseJwt = await refreshJwt.fire({
|
|
||||||
refreshToken: refreshTokenArg,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (responseJwt.jwt) {
|
if (jwt) {
|
||||||
this.domtools.convenience.smartdelay.delayFor(delayDispatchMillisArg).then(() => {
|
this.domtools.convenience.smartdelay.delayFor(delayDispatchMillisArg).then(() => {
|
||||||
this.dispatchJwt(responseJwt.jwt);
|
this.dispatchJwt(jwt);
|
||||||
});
|
});
|
||||||
return responseJwt.jwt;
|
return jwt;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,15 +488,15 @@ export class IdpRegistrationStepper extends DeesElement {
|
|||||||
username: this.storedData.email,
|
username: this.storedData.email,
|
||||||
password: eventArg.detail.data.password,
|
password: eventArg.detail.data.password,
|
||||||
});
|
});
|
||||||
this.storedData.refreshToken = loginResponse.refreshToken;
|
|
||||||
|
|
||||||
deesForm.setStatus('pending', 'Obtaining JWT...');
|
deesForm.setStatus('pending', 'Obtaining JWT...');
|
||||||
const jwtResponse = await idpState.idpClient.requests.obtainJwt.fire({
|
const jwt = await idpState.idpClient.refreshJwt(loginResponse.refreshToken);
|
||||||
refreshToken: this.storedData.refreshToken,
|
|
||||||
});
|
if (!jwt) {
|
||||||
|
deesForm.setStatus('error', 'Failed to establish a login session.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
deesForm.setStatus('success', 'Ok! Lets Go!');
|
deesForm.setStatus('success', 'Ok! Lets Go!');
|
||||||
await idpState.idpClient.setJwt(jwtResponse.jwt);
|
|
||||||
idpState.domtools.router.pushUrl('/account');
|
idpState.domtools.router.pushUrl('/account');
|
||||||
}, { signal });
|
}, { signal });
|
||||||
},
|
},
|
||||||
|
|||||||
+19
-2
@@ -251,6 +251,23 @@ pnpm build
|
|||||||
|
|
||||||
The bundled output is served from `dist_ts_web/` by the TypedServer.
|
The bundled output is served from `dist_ts_web/` by the TypedServer.
|
||||||
|
|
||||||
## License
|
## License and Legal Information
|
||||||
|
|
||||||
MIT - See the main repository for full license details.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|||||||
+3
-1
@@ -4,7 +4,9 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"strict": false
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"dist_*/**/*.d.ts"
|
"dist_*/**/*.d.ts"
|
||||||
|
|||||||
Reference in New Issue
Block a user