ofauth-shared-core 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/OfauthCore.d.ts +48 -0
- package/dist/OfauthCore.js +200 -0
- package/dist/contracts/AuthAuditContract.d.ts +25 -0
- package/dist/contracts/AuthAuditContract.js +16 -0
- package/dist/contracts/AuthErrorContract.d.ts +5 -0
- package/dist/contracts/AuthErrorContract.js +2 -0
- package/dist/contracts/AuthPolicyContract.d.ts +25 -0
- package/dist/contracts/AuthPolicyContract.js +2 -0
- package/dist/contracts/ContextContract.d.ts +27 -0
- package/dist/contracts/ContextContract.js +8 -0
- package/dist/contracts/IdentityContract.d.ts +36 -0
- package/dist/contracts/IdentityContract.js +2 -0
- package/dist/contracts/SessionContract.d.ts +25 -0
- package/dist/contracts/SessionContract.js +10 -0
- package/dist/data/applyPendingMigrations.d.ts +2 -0
- package/dist/data/applyPendingMigrations.js +30 -0
- package/dist/data/migrations.d.ts +2 -0
- package/dist/data/migrations.js +47 -0
- package/dist/data/schemas.d.ts +11 -0
- package/dist/data/schemas.js +109 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +36 -0
- package/dist/runtime/ContractStage.d.ts +2 -0
- package/dist/runtime/ContractStage.js +4 -0
- package/dist/services/AuthAuditService.d.ts +7 -0
- package/dist/services/AuthAuditService.js +2 -0
- package/dist/services/AuthHostComposition.d.ts +40 -0
- package/dist/services/AuthHostComposition.js +100 -0
- package/dist/services/AuthPolicyService.d.ts +9 -0
- package/dist/services/AuthPolicyService.js +2 -0
- package/dist/services/ContextService.d.ts +6 -0
- package/dist/services/ContextService.js +2 -0
- package/dist/services/IdentityService.d.ts +15 -0
- package/dist/services/IdentityService.js +2 -0
- package/dist/services/SessionService.d.ts +7 -0
- package/dist/services/SessionService.js +2 -0
- package/dist/services/createContractOnlyOfauthServices.d.ts +13 -0
- package/dist/services/createContractOnlyOfauthServices.js +82 -0
- package/dist/services/createDbAdapterOfauthServices.d.ts +20 -0
- package/dist/services/createDbAdapterOfauthServices.js +22 -0
- package/dist/services/errors.d.ts +8 -0
- package/dist/services/errors.js +20 -0
- package/dist/services/impl/DbAdapterAuthAuditService.d.ts +14 -0
- package/dist/services/impl/DbAdapterAuthAuditService.js +107 -0
- package/dist/services/impl/DbAdapterAuthPolicyService.d.ts +17 -0
- package/dist/services/impl/DbAdapterAuthPolicyService.js +114 -0
- package/dist/services/impl/DbAdapterContextService.d.ts +13 -0
- package/dist/services/impl/DbAdapterContextService.js +79 -0
- package/dist/services/impl/DbAdapterIdentityService.d.ts +23 -0
- package/dist/services/impl/DbAdapterIdentityService.js +146 -0
- package/dist/services/impl/DbAdapterSessionService.d.ts +14 -0
- package/dist/services/impl/DbAdapterSessionService.js +63 -0
- package/dist/services/impl/runtimeSupport.d.ts +95 -0
- package/dist/services/impl/runtimeSupport.js +112 -0
- package/package.json +37 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DbAdapterAuthAuditService = void 0;
|
|
4
|
+
const schemas_1 = require("../../data/schemas");
|
|
5
|
+
const runtimeSupport_1 = require("./runtimeSupport");
|
|
6
|
+
class DbAdapterAuthAuditService {
|
|
7
|
+
constructor(db, options) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.newId = (0, runtimeSupport_1.makeIdFactory)('auth-audit');
|
|
11
|
+
}
|
|
12
|
+
async appendEvent(event) {
|
|
13
|
+
const row = {
|
|
14
|
+
id: event.eventId || this.newId(),
|
|
15
|
+
eventType: event.eventType,
|
|
16
|
+
identityId: event.identityId ?? null,
|
|
17
|
+
sessionId: event.sessionId ?? null,
|
|
18
|
+
tenantId: event.scopeRef?.tenantId ?? null,
|
|
19
|
+
branchId: event.scopeRef?.branchId ?? null,
|
|
20
|
+
scopeAttributes: event.scopeRef?.attributes ?? null,
|
|
21
|
+
result: event.result,
|
|
22
|
+
reasonCode: event.reasonCode ?? null,
|
|
23
|
+
timestamp: event.timestamp,
|
|
24
|
+
syncStatus: event.syncStatus ?? 'pending',
|
|
25
|
+
replayedAt: event.replayedAt ?? null,
|
|
26
|
+
version: 1,
|
|
27
|
+
lastModified: (0, runtimeSupport_1.nowIso)(),
|
|
28
|
+
deleted: false,
|
|
29
|
+
};
|
|
30
|
+
await this.db.create(schemas_1.OFAUTH_TABLES.auditEvents, row);
|
|
31
|
+
await (0, runtimeSupport_1.emitAuthActivity)(this.options, event);
|
|
32
|
+
}
|
|
33
|
+
async queryEvents(query) {
|
|
34
|
+
const rows = await this.db.query(schemas_1.OFAUTH_TABLES.auditEvents, {
|
|
35
|
+
filters: { deleted: false },
|
|
36
|
+
sort: [{ field: 'timestamp', direction: 'desc' }],
|
|
37
|
+
});
|
|
38
|
+
return rows
|
|
39
|
+
.filter((row) => (query.identityId ? row.identityId === query.identityId : true))
|
|
40
|
+
.filter((row) => (query.sessionId ? row.sessionId === query.sessionId : true))
|
|
41
|
+
.filter((row) => (query.eventTypes?.length ? query.eventTypes.includes(row.eventType) : true))
|
|
42
|
+
.filter((row) => (query.fromTimestamp ? row.timestamp >= query.fromTimestamp : true))
|
|
43
|
+
.filter((row) => (query.toTimestamp ? row.timestamp <= query.toTimestamp : true))
|
|
44
|
+
.map((row) => ({
|
|
45
|
+
eventId: row.id,
|
|
46
|
+
eventType: row.eventType,
|
|
47
|
+
identityId: row.identityId ?? undefined,
|
|
48
|
+
sessionId: row.sessionId ?? undefined,
|
|
49
|
+
scopeRef: row.tenantId || row.branchId || row.scopeAttributes
|
|
50
|
+
? {
|
|
51
|
+
...(row.tenantId ? { tenantId: row.tenantId } : {}),
|
|
52
|
+
...(row.branchId ? { branchId: row.branchId } : {}),
|
|
53
|
+
...(row.scopeAttributes ? { attributes: row.scopeAttributes } : {}),
|
|
54
|
+
}
|
|
55
|
+
: undefined,
|
|
56
|
+
result: row.result,
|
|
57
|
+
reasonCode: row.reasonCode ?? undefined,
|
|
58
|
+
timestamp: row.timestamp,
|
|
59
|
+
syncStatus: row.syncStatus ?? 'pending',
|
|
60
|
+
replayedAt: row.replayedAt ?? undefined,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
async listPendingReplay(query = {}) {
|
|
64
|
+
const rows = await this.db.query(schemas_1.OFAUTH_TABLES.auditEvents, {
|
|
65
|
+
filters: { deleted: false },
|
|
66
|
+
sort: [{ field: 'timestamp', direction: 'asc' }],
|
|
67
|
+
});
|
|
68
|
+
return rows
|
|
69
|
+
.filter((row) => row.syncStatus !== 'replayed')
|
|
70
|
+
.slice(0, query.limit ?? rows.length)
|
|
71
|
+
.map((row) => ({
|
|
72
|
+
eventId: row.id,
|
|
73
|
+
eventType: row.eventType,
|
|
74
|
+
identityId: row.identityId ?? undefined,
|
|
75
|
+
sessionId: row.sessionId ?? undefined,
|
|
76
|
+
scopeRef: row.tenantId || row.branchId || row.scopeAttributes
|
|
77
|
+
? {
|
|
78
|
+
...(row.tenantId ? { tenantId: row.tenantId } : {}),
|
|
79
|
+
...(row.branchId ? { branchId: row.branchId } : {}),
|
|
80
|
+
...(row.scopeAttributes ? { attributes: row.scopeAttributes } : {}),
|
|
81
|
+
}
|
|
82
|
+
: undefined,
|
|
83
|
+
result: row.result,
|
|
84
|
+
reasonCode: row.reasonCode ?? undefined,
|
|
85
|
+
timestamp: row.timestamp,
|
|
86
|
+
syncStatus: row.syncStatus ?? 'pending',
|
|
87
|
+
replayedAt: row.replayedAt ?? undefined,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
async markReplayed(eventIds, replayedAt = (0, runtimeSupport_1.nowIso)()) {
|
|
91
|
+
let updatedCount = 0;
|
|
92
|
+
for (const eventId of eventIds) {
|
|
93
|
+
const row = await this.db.get(schemas_1.OFAUTH_TABLES.auditEvents, eventId);
|
|
94
|
+
if (!row || row.deleted || row.syncStatus === 'replayed')
|
|
95
|
+
continue;
|
|
96
|
+
await this.db.update(schemas_1.OFAUTH_TABLES.auditEvents, eventId, {
|
|
97
|
+
syncStatus: 'replayed',
|
|
98
|
+
replayedAt,
|
|
99
|
+
version: row.version + 1,
|
|
100
|
+
lastModified: (0, runtimeSupport_1.nowIso)(),
|
|
101
|
+
});
|
|
102
|
+
updatedCount += 1;
|
|
103
|
+
}
|
|
104
|
+
return updatedCount;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.DbAdapterAuthAuditService = DbAdapterAuthAuditService;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DbAdapter } from 'ofcore';
|
|
2
|
+
import type { AuthPolicySnapshot, StepUpEvaluation, StepUpRequirement } from '../../contracts/AuthPolicyContract';
|
|
3
|
+
import type { SessionRef } from '../../contracts/SessionContract';
|
|
4
|
+
import type { AuthAuditService } from '../AuthAuditService';
|
|
5
|
+
import type { AuthPolicyService } from '../AuthPolicyService';
|
|
6
|
+
export declare class DbAdapterAuthPolicyService implements AuthPolicyService {
|
|
7
|
+
private readonly db;
|
|
8
|
+
private readonly authAuditService;
|
|
9
|
+
private readonly newEventId;
|
|
10
|
+
constructor(db: DbAdapter, authAuditService: AuthAuditService);
|
|
11
|
+
private getActiveSessionOrThrow;
|
|
12
|
+
getPolicySnapshot(): Promise<AuthPolicySnapshot>;
|
|
13
|
+
getStepUpRequirement(actionRef: string): Promise<StepUpRequirement | null>;
|
|
14
|
+
evaluateStepUp(sessionId: string, actionRef: string): Promise<StepUpEvaluation>;
|
|
15
|
+
enforceStepUp(sessionId: string, actionRef: string): Promise<void>;
|
|
16
|
+
markStepUpPassed(sessionId: string, actionRef: string): Promise<SessionRef>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DbAdapterAuthPolicyService = void 0;
|
|
4
|
+
const SessionContract_1 = require("../../contracts/SessionContract");
|
|
5
|
+
const schemas_1 = require("../../data/schemas");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const runtimeSupport_1 = require("./runtimeSupport");
|
|
8
|
+
class DbAdapterAuthPolicyService {
|
|
9
|
+
constructor(db, authAuditService) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
this.authAuditService = authAuditService;
|
|
12
|
+
this.newEventId = (0, runtimeSupport_1.makeIdFactory)('auth-event');
|
|
13
|
+
}
|
|
14
|
+
async getActiveSessionOrThrow(sessionId) {
|
|
15
|
+
const row = await this.db.get(schemas_1.OFAUTH_TABLES.sessions, sessionId);
|
|
16
|
+
if (!row || row.deleted || row.revokedAt || (0, runtimeSupport_1.isPast)(row.expiresAt)) {
|
|
17
|
+
throw (0, errors_1.unauthorized)('AUTH_SESSION_NOT_FOUND', 'Session not found');
|
|
18
|
+
}
|
|
19
|
+
return row;
|
|
20
|
+
}
|
|
21
|
+
async getPolicySnapshot() {
|
|
22
|
+
const profiles = await this.db.query(schemas_1.OFAUTH_TABLES.policyProfiles, {
|
|
23
|
+
filters: { deleted: false },
|
|
24
|
+
sort: [{ field: 'lastModified', direction: 'desc' }],
|
|
25
|
+
});
|
|
26
|
+
const stepups = await this.db.query(schemas_1.OFAUTH_TABLES.policyStepups, {
|
|
27
|
+
filters: { deleted: false },
|
|
28
|
+
sort: [{ field: 'lastModified', direction: 'desc' }],
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
profiles: profiles.length > 0
|
|
32
|
+
? profiles.map((p) => ({
|
|
33
|
+
profileId: p.profileId,
|
|
34
|
+
defaultAssuranceLevel: p.defaultAssuranceLevel,
|
|
35
|
+
lockoutPolicyRef: p.lockoutPolicyRef,
|
|
36
|
+
}))
|
|
37
|
+
: (0, runtimeSupport_1.fallbackPolicyProfiles)(),
|
|
38
|
+
stepUpRequirements: stepups.map((s) => ({
|
|
39
|
+
actionRef: s.actionRef,
|
|
40
|
+
requiredAssuranceLevel: s.requiredAssuranceLevel,
|
|
41
|
+
reauthWindowSeconds: s.reauthWindowSeconds ?? undefined,
|
|
42
|
+
})),
|
|
43
|
+
version: '1',
|
|
44
|
+
updatedAt: (0, runtimeSupport_1.nowIso)(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async getStepUpRequirement(actionRef) {
|
|
48
|
+
const row = await this.db.query(schemas_1.OFAUTH_TABLES.policyStepups, {
|
|
49
|
+
filters: { actionRef, deleted: false },
|
|
50
|
+
limit: 1,
|
|
51
|
+
});
|
|
52
|
+
if (!row[0])
|
|
53
|
+
return null;
|
|
54
|
+
return {
|
|
55
|
+
actionRef: row[0].actionRef,
|
|
56
|
+
requiredAssuranceLevel: row[0].requiredAssuranceLevel,
|
|
57
|
+
reauthWindowSeconds: row[0].reauthWindowSeconds ?? undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async evaluateStepUp(sessionId, actionRef) {
|
|
61
|
+
const session = await this.getActiveSessionOrThrow(sessionId);
|
|
62
|
+
const requirement = await this.getStepUpRequirement(actionRef);
|
|
63
|
+
const requiredAssuranceLevel = requirement?.requiredAssuranceLevel ?? 'basic';
|
|
64
|
+
const requiresStepUp = !(0, SessionContract_1.isAssuranceLevelAtLeast)(session.assuranceLevel, requiredAssuranceLevel);
|
|
65
|
+
return {
|
|
66
|
+
actionRef,
|
|
67
|
+
requiredAssuranceLevel,
|
|
68
|
+
currentAssuranceLevel: session.assuranceLevel,
|
|
69
|
+
requiresStepUp,
|
|
70
|
+
reauthWindowSeconds: requirement?.reauthWindowSeconds,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async enforceStepUp(sessionId, actionRef) {
|
|
74
|
+
const session = await this.getActiveSessionOrThrow(sessionId);
|
|
75
|
+
const evalResult = await this.evaluateStepUp(sessionId, actionRef);
|
|
76
|
+
if (!evalResult.requiresStepUp)
|
|
77
|
+
return;
|
|
78
|
+
await this.authAuditService.appendEvent({
|
|
79
|
+
eventId: this.newEventId(),
|
|
80
|
+
eventType: 'STEP_UP_CHALLENGED',
|
|
81
|
+
identityId: session.identityId,
|
|
82
|
+
sessionId,
|
|
83
|
+
result: 'failed',
|
|
84
|
+
reasonCode: 'AUTH_STEP_UP_REQUIRED',
|
|
85
|
+
timestamp: (0, runtimeSupport_1.nowIso)(),
|
|
86
|
+
});
|
|
87
|
+
throw (0, errors_1.unauthorized)('AUTH_STEP_UP_REQUIRED', `Step-up required for action '${actionRef}' (needs ${evalResult.requiredAssuranceLevel})`);
|
|
88
|
+
}
|
|
89
|
+
async markStepUpPassed(sessionId, actionRef) {
|
|
90
|
+
const session = await this.getActiveSessionOrThrow(sessionId);
|
|
91
|
+
const requirement = await this.getStepUpRequirement(actionRef);
|
|
92
|
+
const targetAssurance = requirement?.requiredAssuranceLevel ?? 'elevated';
|
|
93
|
+
await this.db.update(schemas_1.OFAUTH_TABLES.sessions, sessionId, {
|
|
94
|
+
assuranceLevel: targetAssurance,
|
|
95
|
+
version: session.version + 1,
|
|
96
|
+
lastModified: (0, runtimeSupport_1.nowIso)(),
|
|
97
|
+
});
|
|
98
|
+
await this.authAuditService.appendEvent({
|
|
99
|
+
eventId: this.newEventId(),
|
|
100
|
+
eventType: 'STEP_UP_PASSED',
|
|
101
|
+
identityId: session.identityId,
|
|
102
|
+
sessionId,
|
|
103
|
+
result: 'success',
|
|
104
|
+
reasonCode: actionRef,
|
|
105
|
+
timestamp: (0, runtimeSupport_1.nowIso)(),
|
|
106
|
+
});
|
|
107
|
+
const active = await this.db.get(schemas_1.OFAUTH_TABLES.sessions, sessionId);
|
|
108
|
+
const assignment = active ? await (0, runtimeSupport_1.getAssignmentById)(this.db, active.activeAssignmentId) : null;
|
|
109
|
+
if (!active)
|
|
110
|
+
throw (0, errors_1.unauthorized)('AUTH_SESSION_NOT_FOUND', 'Session not found');
|
|
111
|
+
return (0, runtimeSupport_1.toSessionRef)(active, assignment ?? undefined);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.DbAdapterAuthPolicyService = DbAdapterAuthPolicyService;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DbAdapter } from 'ofcore';
|
|
2
|
+
import type { ActiveContext, ContextAssignment, ContextSwitchRequest, ContextSwitchResult } from '../../contracts/ContextContract';
|
|
3
|
+
import type { AuthAuditService } from '../AuthAuditService';
|
|
4
|
+
import type { ContextService } from '../ContextService';
|
|
5
|
+
export declare class DbAdapterContextService implements ContextService {
|
|
6
|
+
private readonly db;
|
|
7
|
+
private readonly authAuditService;
|
|
8
|
+
private readonly newEventId;
|
|
9
|
+
constructor(db: DbAdapter, authAuditService: AuthAuditService);
|
|
10
|
+
listAssignments(identityId: string): Promise<ContextAssignment[]>;
|
|
11
|
+
getActiveContext(sessionId: string): Promise<ActiveContext | null>;
|
|
12
|
+
switchContext(input: ContextSwitchRequest): Promise<ContextSwitchResult>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DbAdapterContextService = void 0;
|
|
4
|
+
const schemas_1 = require("../../data/schemas");
|
|
5
|
+
const errors_1 = require("../errors");
|
|
6
|
+
const runtimeSupport_1 = require("./runtimeSupport");
|
|
7
|
+
class DbAdapterContextService {
|
|
8
|
+
constructor(db, authAuditService) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
this.authAuditService = authAuditService;
|
|
11
|
+
this.newEventId = (0, runtimeSupport_1.makeIdFactory)('auth-event');
|
|
12
|
+
}
|
|
13
|
+
async listAssignments(identityId) {
|
|
14
|
+
const rows = await this.db.query(schemas_1.OFAUTH_TABLES.assignments, {
|
|
15
|
+
filters: { identityId, deleted: false },
|
|
16
|
+
sort: [{ field: 'lastModified', direction: 'desc' }],
|
|
17
|
+
});
|
|
18
|
+
return rows.map(runtimeSupport_1.toAssignment);
|
|
19
|
+
}
|
|
20
|
+
async getActiveContext(sessionId) {
|
|
21
|
+
const session = await this.db.get(schemas_1.OFAUTH_TABLES.sessions, sessionId);
|
|
22
|
+
if (!session || session.deleted || session.revokedAt || !session.activeAssignmentId)
|
|
23
|
+
return null;
|
|
24
|
+
const assignment = await (0, runtimeSupport_1.getAssignmentById)(this.db, session.activeAssignmentId);
|
|
25
|
+
if (!assignment)
|
|
26
|
+
return null;
|
|
27
|
+
return {
|
|
28
|
+
sessionId,
|
|
29
|
+
assignmentId: assignment.id,
|
|
30
|
+
scopeRef: {
|
|
31
|
+
...(assignment.tenantId ? { tenantId: assignment.tenantId } : {}),
|
|
32
|
+
...(assignment.branchId ? { branchId: assignment.branchId } : {}),
|
|
33
|
+
...(assignment.scopeAttributes ? { attributes: assignment.scopeAttributes } : {}),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async switchContext(input) {
|
|
38
|
+
const session = await this.db.get(schemas_1.OFAUTH_TABLES.sessions, input.sessionId);
|
|
39
|
+
if (!session || session.deleted || session.revokedAt)
|
|
40
|
+
throw (0, errors_1.unauthorized)('AUTH_SESSION_NOT_FOUND', 'Session not found');
|
|
41
|
+
const target = await (0, runtimeSupport_1.getAssignmentById)(this.db, input.targetAssignmentId);
|
|
42
|
+
if (!target || target.identityId !== session.identityId) {
|
|
43
|
+
throw (0, errors_1.unauthorized)('AUTH_CONTEXT_FORBIDDEN', 'Requested context is not assigned to this identity');
|
|
44
|
+
}
|
|
45
|
+
const updatedSession = await this.db.update(schemas_1.OFAUTH_TABLES.sessions, input.sessionId, {
|
|
46
|
+
activeAssignmentId: target.id,
|
|
47
|
+
version: session.version + 1,
|
|
48
|
+
lastModified: (0, runtimeSupport_1.nowIso)(),
|
|
49
|
+
});
|
|
50
|
+
await this.authAuditService.appendEvent({
|
|
51
|
+
eventId: this.newEventId(),
|
|
52
|
+
eventType: 'CONTEXT_SWITCHED',
|
|
53
|
+
identityId: session.identityId,
|
|
54
|
+
sessionId: input.sessionId,
|
|
55
|
+
scopeRef: {
|
|
56
|
+
...(target.tenantId ? { tenantId: target.tenantId } : {}),
|
|
57
|
+
...(target.branchId ? { branchId: target.branchId } : {}),
|
|
58
|
+
...(target.scopeAttributes ? { attributes: target.scopeAttributes } : {}),
|
|
59
|
+
},
|
|
60
|
+
result: 'success',
|
|
61
|
+
reasonCode: input.reasonCode,
|
|
62
|
+
timestamp: updatedSession.lastModified,
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
activeContext: {
|
|
66
|
+
sessionId: input.sessionId,
|
|
67
|
+
assignmentId: target.id,
|
|
68
|
+
scopeRef: {
|
|
69
|
+
...(target.tenantId ? { tenantId: target.tenantId } : {}),
|
|
70
|
+
...(target.branchId ? { branchId: target.branchId } : {}),
|
|
71
|
+
...(target.scopeAttributes ? { attributes: target.scopeAttributes } : {}),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
requiresStepUp: false,
|
|
75
|
+
reasonCode: input.reasonCode,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.DbAdapterContextService = DbAdapterContextService;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DbAdapter } from 'ofcore';
|
|
2
|
+
import type { CredentialVerifyRequest, CredentialVerifyResult } from '../../contracts/IdentityContract';
|
|
3
|
+
import type { AuthAuditService } from '../AuthAuditService';
|
|
4
|
+
import type { ContextService } from '../ContextService';
|
|
5
|
+
import type { IdentityService, LoginRequest, LoginResult } from '../IdentityService';
|
|
6
|
+
import type { SessionService } from '../SessionService';
|
|
7
|
+
import type { ServiceRuntimeOptions } from './runtimeSupport';
|
|
8
|
+
export declare class DbAdapterIdentityService implements IdentityService {
|
|
9
|
+
private readonly db;
|
|
10
|
+
private readonly options;
|
|
11
|
+
private readonly sessionService;
|
|
12
|
+
private readonly contextService;
|
|
13
|
+
private readonly authAuditService;
|
|
14
|
+
private readonly newEventId;
|
|
15
|
+
constructor(db: DbAdapter, options: ServiceRuntimeOptions, sessionService: SessionService, contextService: ContextService, authAuditService: AuthAuditService);
|
|
16
|
+
private findIdentityByPrincipal;
|
|
17
|
+
private persistFailedAttempt;
|
|
18
|
+
private clearFailedAttempt;
|
|
19
|
+
private emitAudit;
|
|
20
|
+
verifyCredential(input: CredentialVerifyRequest): Promise<CredentialVerifyResult>;
|
|
21
|
+
login(input: LoginRequest): Promise<LoginResult>;
|
|
22
|
+
logout(sessionId: string): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DbAdapterIdentityService = void 0;
|
|
4
|
+
const schemas_1 = require("../../data/schemas");
|
|
5
|
+
const errors_1 = require("../errors");
|
|
6
|
+
const runtimeSupport_1 = require("./runtimeSupport");
|
|
7
|
+
class DbAdapterIdentityService {
|
|
8
|
+
constructor(db, options, sessionService, contextService, authAuditService) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.sessionService = sessionService;
|
|
12
|
+
this.contextService = contextService;
|
|
13
|
+
this.authAuditService = authAuditService;
|
|
14
|
+
this.newEventId = (0, runtimeSupport_1.makeIdFactory)('auth-event');
|
|
15
|
+
}
|
|
16
|
+
async findIdentityByPrincipal(principal) {
|
|
17
|
+
const rows = await this.db.query(schemas_1.OFAUTH_TABLES.identities, {
|
|
18
|
+
filters: { principal, deleted: false },
|
|
19
|
+
limit: 1,
|
|
20
|
+
});
|
|
21
|
+
return rows[0] ?? null;
|
|
22
|
+
}
|
|
23
|
+
async persistFailedAttempt(identity) {
|
|
24
|
+
const failedAttempts = identity.failedAttempts + 1;
|
|
25
|
+
const lockoutTriggered = failedAttempts >= runtimeSupport_1.DEFAULT_LOCKOUT_THRESHOLD;
|
|
26
|
+
const lockoutUntil = lockoutTriggered ? (0, runtimeSupport_1.plusSecondsIso)((0, runtimeSupport_1.nowIso)(), runtimeSupport_1.DEFAULT_LOCKOUT_COOLDOWN_SECONDS) : null;
|
|
27
|
+
const status = lockoutTriggered ? 'locked' : identity.status;
|
|
28
|
+
return this.db.update(schemas_1.OFAUTH_TABLES.identities, identity.id, {
|
|
29
|
+
failedAttempts,
|
|
30
|
+
lockoutUntil,
|
|
31
|
+
status,
|
|
32
|
+
version: identity.version + 1,
|
|
33
|
+
lastModified: (0, runtimeSupport_1.nowIso)(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async clearFailedAttempt(identity) {
|
|
37
|
+
if (identity.failedAttempts === 0 && !identity.lockoutUntil && identity.status === 'active')
|
|
38
|
+
return;
|
|
39
|
+
await this.db.update(schemas_1.OFAUTH_TABLES.identities, identity.id, {
|
|
40
|
+
failedAttempts: 0,
|
|
41
|
+
lockoutUntil: null,
|
|
42
|
+
status: 'active',
|
|
43
|
+
version: identity.version + 1,
|
|
44
|
+
lastModified: (0, runtimeSupport_1.nowIso)(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async emitAudit(event) {
|
|
48
|
+
await this.authAuditService.appendEvent(event);
|
|
49
|
+
}
|
|
50
|
+
async verifyCredential(input) {
|
|
51
|
+
const identity = await this.findIdentityByPrincipal(input.principal);
|
|
52
|
+
if (!identity) {
|
|
53
|
+
await this.emitAudit({
|
|
54
|
+
eventId: this.newEventId(),
|
|
55
|
+
eventType: 'LOGIN_FAILED',
|
|
56
|
+
result: 'failed',
|
|
57
|
+
reasonCode: 'INVALID_CREDENTIAL',
|
|
58
|
+
timestamp: (0, runtimeSupport_1.nowIso)(),
|
|
59
|
+
});
|
|
60
|
+
return { ok: false, reasonCode: 'INVALID_CREDENTIAL', retryable: true };
|
|
61
|
+
}
|
|
62
|
+
if (identity.status === 'disabled') {
|
|
63
|
+
await this.emitAudit({
|
|
64
|
+
eventId: this.newEventId(),
|
|
65
|
+
eventType: 'LOGIN_FAILED',
|
|
66
|
+
identityId: identity.id,
|
|
67
|
+
result: 'failed',
|
|
68
|
+
reasonCode: 'IDENTITY_DISABLED',
|
|
69
|
+
timestamp: (0, runtimeSupport_1.nowIso)(),
|
|
70
|
+
});
|
|
71
|
+
return { ok: false, reasonCode: 'IDENTITY_DISABLED', retryable: false };
|
|
72
|
+
}
|
|
73
|
+
if (identity.lockoutUntil && !(0, runtimeSupport_1.isPast)(identity.lockoutUntil)) {
|
|
74
|
+
await this.emitAudit({
|
|
75
|
+
eventId: this.newEventId(),
|
|
76
|
+
eventType: 'LOCKOUT_TRIGGERED',
|
|
77
|
+
identityId: identity.id,
|
|
78
|
+
result: 'failed',
|
|
79
|
+
reasonCode: 'IDENTITY_LOCKED',
|
|
80
|
+
timestamp: (0, runtimeSupport_1.nowIso)(),
|
|
81
|
+
});
|
|
82
|
+
return { ok: false, reasonCode: 'IDENTITY_LOCKED', retryable: false };
|
|
83
|
+
}
|
|
84
|
+
const verified = await (0, runtimeSupport_1.verifySecret)(this.options.platformAdapter, input.secret, identity.secretHash);
|
|
85
|
+
if (!verified) {
|
|
86
|
+
const afterFail = await this.persistFailedAttempt(identity);
|
|
87
|
+
const reasonCode = afterFail.status === 'locked' ? 'IDENTITY_LOCKED' : 'INVALID_CREDENTIAL';
|
|
88
|
+
await this.emitAudit({
|
|
89
|
+
eventId: this.newEventId(),
|
|
90
|
+
eventType: reasonCode === 'IDENTITY_LOCKED' ? 'LOCKOUT_TRIGGERED' : 'LOGIN_FAILED',
|
|
91
|
+
identityId: identity.id,
|
|
92
|
+
result: 'failed',
|
|
93
|
+
reasonCode,
|
|
94
|
+
timestamp: (0, runtimeSupport_1.nowIso)(),
|
|
95
|
+
});
|
|
96
|
+
return { ok: false, reasonCode, retryable: reasonCode === 'INVALID_CREDENTIAL' };
|
|
97
|
+
}
|
|
98
|
+
await this.clearFailedAttempt(identity);
|
|
99
|
+
const assignments = await this.contextService.listAssignments(identity.id);
|
|
100
|
+
await this.emitAudit({
|
|
101
|
+
eventId: this.newEventId(),
|
|
102
|
+
eventType: 'LOGIN_SUCCESS',
|
|
103
|
+
identityId: identity.id,
|
|
104
|
+
scopeRef: input.scopeRef,
|
|
105
|
+
result: 'success',
|
|
106
|
+
timestamp: (0, runtimeSupport_1.nowIso)(),
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
identity: (0, runtimeSupport_1.toIdentityRef)(identity),
|
|
111
|
+
assignments,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async login(input) {
|
|
115
|
+
const verified = await this.verifyCredential({
|
|
116
|
+
principal: input.principal,
|
|
117
|
+
secret: input.secret,
|
|
118
|
+
verifyMethod: input.verifyMethod,
|
|
119
|
+
});
|
|
120
|
+
if (!verified.ok) {
|
|
121
|
+
throw (0, errors_1.unauthorized)('AUTH_INVALID_CREDENTIAL', `Authentication failed: ${verified.reasonCode}`);
|
|
122
|
+
}
|
|
123
|
+
const defaultAssignment = [...verified.assignments].sort((a, b) => a.assignmentId.localeCompare(b.assignmentId))[0];
|
|
124
|
+
const session = await this.sessionService.createSession({
|
|
125
|
+
identityId: verified.identity.identityId,
|
|
126
|
+
assignmentId: defaultAssignment?.assignmentId,
|
|
127
|
+
assuranceLevel: 'basic',
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
identity: verified.identity,
|
|
131
|
+
sessionId: session.sessionId,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async logout(sessionId) {
|
|
135
|
+
await this.sessionService.revokeSession({ sessionId, reasonCode: 'LOGOUT' });
|
|
136
|
+
await this.emitAudit({
|
|
137
|
+
eventId: this.newEventId(),
|
|
138
|
+
eventType: 'SESSION_REVOKED',
|
|
139
|
+
sessionId,
|
|
140
|
+
result: 'success',
|
|
141
|
+
reasonCode: 'LOGOUT',
|
|
142
|
+
timestamp: (0, runtimeSupport_1.nowIso)(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.DbAdapterIdentityService = DbAdapterIdentityService;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DbAdapter } from 'ofcore';
|
|
2
|
+
import type { CreateSessionRequest, RefreshSessionRequest, RevokeSessionRequest, SessionRef } from '../../contracts/SessionContract';
|
|
3
|
+
import type { SessionService } from '../SessionService';
|
|
4
|
+
import type { ServiceRuntimeOptions } from './runtimeSupport';
|
|
5
|
+
export declare class DbAdapterSessionService implements SessionService {
|
|
6
|
+
private readonly db;
|
|
7
|
+
private readonly options;
|
|
8
|
+
private readonly newId;
|
|
9
|
+
constructor(db: DbAdapter, options: ServiceRuntimeOptions);
|
|
10
|
+
createSession(input: CreateSessionRequest): Promise<SessionRef>;
|
|
11
|
+
refreshSession(input: RefreshSessionRequest): Promise<SessionRef>;
|
|
12
|
+
revokeSession(input: RevokeSessionRequest): Promise<void>;
|
|
13
|
+
getActiveSession(sessionId: string): Promise<SessionRef | null>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DbAdapterSessionService = void 0;
|
|
4
|
+
const schemas_1 = require("../../data/schemas");
|
|
5
|
+
const errors_1 = require("../errors");
|
|
6
|
+
const runtimeSupport_1 = require("./runtimeSupport");
|
|
7
|
+
class DbAdapterSessionService {
|
|
8
|
+
constructor(db, options) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.newId = (0, runtimeSupport_1.makeIdFactory)('auth-session');
|
|
12
|
+
}
|
|
13
|
+
async createSession(input) {
|
|
14
|
+
const issuedAt = (0, runtimeSupport_1.nowIso)();
|
|
15
|
+
const expiresAt = (0, runtimeSupport_1.plusSecondsIso)(issuedAt, input.ttlSeconds ?? runtimeSupport_1.DEFAULT_SESSION_TTL_SECONDS);
|
|
16
|
+
const created = await this.db.create(schemas_1.OFAUTH_TABLES.sessions, {
|
|
17
|
+
id: this.newId(),
|
|
18
|
+
identityId: input.identityId,
|
|
19
|
+
activeAssignmentId: input.assignmentId ?? null,
|
|
20
|
+
assuranceLevel: input.assuranceLevel ?? 'basic',
|
|
21
|
+
issuedAt,
|
|
22
|
+
expiresAt,
|
|
23
|
+
revokedAt: null,
|
|
24
|
+
version: 1,
|
|
25
|
+
lastModified: issuedAt,
|
|
26
|
+
deleted: false,
|
|
27
|
+
});
|
|
28
|
+
const assignment = await (0, runtimeSupport_1.getAssignmentById)(this.db, created.activeAssignmentId);
|
|
29
|
+
this.options.logger?.logInfo('[ofauth] session created', { sessionId: created.id, identityId: created.identityId });
|
|
30
|
+
return (0, runtimeSupport_1.toSessionRef)(created, assignment);
|
|
31
|
+
}
|
|
32
|
+
async refreshSession(input) {
|
|
33
|
+
const current = await this.db.get(schemas_1.OFAUTH_TABLES.sessions, input.sessionId);
|
|
34
|
+
if (!current || current.deleted || current.revokedAt)
|
|
35
|
+
throw (0, errors_1.unauthorized)('AUTH_SESSION_NOT_FOUND', 'Session not found');
|
|
36
|
+
const refreshed = await this.db.update(schemas_1.OFAUTH_TABLES.sessions, input.sessionId, {
|
|
37
|
+
expiresAt: (0, runtimeSupport_1.plusSecondsIso)((0, runtimeSupport_1.nowIso)(), input.ttlSeconds ?? runtimeSupport_1.DEFAULT_SESSION_TTL_SECONDS),
|
|
38
|
+
version: current.version + 1,
|
|
39
|
+
lastModified: (0, runtimeSupport_1.nowIso)(),
|
|
40
|
+
});
|
|
41
|
+
const assignment = await (0, runtimeSupport_1.getAssignmentById)(this.db, refreshed.activeAssignmentId);
|
|
42
|
+
return (0, runtimeSupport_1.toSessionRef)(refreshed, assignment);
|
|
43
|
+
}
|
|
44
|
+
async revokeSession(input) {
|
|
45
|
+
const current = await this.db.get(schemas_1.OFAUTH_TABLES.sessions, input.sessionId);
|
|
46
|
+
if (!current || current.deleted || current.revokedAt)
|
|
47
|
+
return;
|
|
48
|
+
await this.db.update(schemas_1.OFAUTH_TABLES.sessions, input.sessionId, {
|
|
49
|
+
revokedAt: (0, runtimeSupport_1.nowIso)(),
|
|
50
|
+
version: current.version + 1,
|
|
51
|
+
lastModified: (0, runtimeSupport_1.nowIso)(),
|
|
52
|
+
});
|
|
53
|
+
this.options.logger?.logInfo('[ofauth] session revoked', { sessionId: input.sessionId, reasonCode: input.reasonCode ?? null });
|
|
54
|
+
}
|
|
55
|
+
async getActiveSession(sessionId) {
|
|
56
|
+
const row = await this.db.get(schemas_1.OFAUTH_TABLES.sessions, sessionId);
|
|
57
|
+
if (!row || row.deleted || row.revokedAt || (0, runtimeSupport_1.isPast)(row.expiresAt))
|
|
58
|
+
return null;
|
|
59
|
+
const assignment = await (0, runtimeSupport_1.getAssignmentById)(this.db, row.activeAssignmentId);
|
|
60
|
+
return (0, runtimeSupport_1.toSessionRef)(row, assignment);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.DbAdapterSessionService = DbAdapterSessionService;
|