@adonis-agora/authkit-server 0.88.1 → 0.90.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/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/src/accounts/provider_identities.d.ts +43 -0
- package/build/src/accounts/provider_identities.js +58 -0
- package/build/src/define_config.d.ts +16 -0
- package/build/src/host/login_notify.js +28 -5
- package/build/src/provider/oidc_service.d.ts +7 -0
- package/build/src/provider/oidc_service.js +11 -0
- package/package.json +3 -3
package/build/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { appKeyEncrypter, lucidAccountStore } from './src/accounts/lucid_account
|
|
|
10
10
|
export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, defaultOrganizationModels, } from './src/accounts/lucid_store/organization_models.js';
|
|
11
11
|
export type { LucidStoresModels, LucidStoresOptions, LucidStoresResult, } from './src/accounts/lucid_stores.js';
|
|
12
12
|
export { lucidStores } from './src/accounts/lucid_stores.js';
|
|
13
|
+
export { type LinkProviderIdentityResult, ProviderIdentities, ProviderIdentitiesUnsupportedError, } from './src/accounts/provider_identities.js';
|
|
13
14
|
export type { PersonalAgentIdentity } from './src/agents/agent_identity.js';
|
|
14
15
|
export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
|
|
15
16
|
export type { PersonalAgentDelegationConfigInput, PersonalAgentRegistration, PersonalAgentResolver, PersonalAgentsConfigInput, ResolvedPersonalAgentsConfig, } from './src/agents/config.js';
|
package/build/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { supportsAccountDeletion, supportsAccountEmailRewrite, supportsAccountIm
|
|
|
7
7
|
export { appKeyEncrypter, lucidAccountStore } from './src/accounts/lucid_account_store.js';
|
|
8
8
|
export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, defaultOrganizationModels, } from './src/accounts/lucid_store/organization_models.js';
|
|
9
9
|
export { lucidStores } from './src/accounts/lucid_stores.js';
|
|
10
|
+
export { ProviderIdentities, ProviderIdentitiesUnsupportedError, } from './src/accounts/provider_identities.js';
|
|
10
11
|
export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
|
|
11
12
|
export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
|
|
12
13
|
export { accessTokenHash, hmacNonceSource, verifyDpopProof, } from './src/agents/poppy/dpop.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type AccountStore, type AuthAccount, type LinkProviderIdentityInput, type ProviderIdentitySummary } from './account_store.js';
|
|
2
|
+
/** O account store não tem account linking (o host não passou `providerIdentityModel`). */
|
|
3
|
+
export declare class ProviderIdentitiesUnsupportedError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
/** Resultado de {@link ProviderIdentities.link}. */
|
|
7
|
+
export interface LinkProviderIdentityResult {
|
|
8
|
+
/**
|
|
9
|
+
* Conta a que a identidade estava ligada antes; `null` se era nova. Diferente do
|
|
10
|
+
* `accountId` pedido = a identidade MUDOU de conta (o "Entrar com <provider>" passa
|
|
11
|
+
* a abrir a conta nova).
|
|
12
|
+
*/
|
|
13
|
+
previousAccountId: string | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Identidades de provider (Google, GitHub, …) das contas: a MESMA ligação que o
|
|
17
|
+
* "Entrar com <provider>" consulta antes de cair no e-mail. Exposto como
|
|
18
|
+
* `authkit.identities` (`@adonis-agora/authkit-server/services/main`) para o host
|
|
19
|
+
* ligar uma conta a um provider fora do login social — ex.: quando a pessoa conecta
|
|
20
|
+
* o Google numa tela de integrações — sem tocar na tabela nem no account store cru.
|
|
21
|
+
*
|
|
22
|
+
* Store sem a capacidade: `supported` é `false` e os demais métodos lançam
|
|
23
|
+
* {@link ProviderIdentitiesUnsupportedError}. Nada falha em silêncio.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ProviderIdentities {
|
|
26
|
+
#private;
|
|
27
|
+
constructor(getStore: () => AccountStore);
|
|
28
|
+
/** O account store tem account linking por provider. */
|
|
29
|
+
get supported(): boolean;
|
|
30
|
+
/** A conta ligada a `(provider, providerUserId)`; `null` se ninguém. */
|
|
31
|
+
find(provider: string, providerUserId: string): Promise<AuthAccount | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Liga a identidade à conta. Idempotente na chave `(provider, providerUserId)`: se
|
|
34
|
+
* ela já estava em OUTRA conta, MUDA para esta (e `previousAccountId` diz de onde).
|
|
35
|
+
* Só chame depois de provar que a pessoa é dona da identidade (OAuth concluído)
|
|
36
|
+
* E da conta (sessão nela).
|
|
37
|
+
*/
|
|
38
|
+
link(input: LinkProviderIdentityInput): Promise<LinkProviderIdentityResult>;
|
|
39
|
+
/** As identidades da conta, sem tokens. */
|
|
40
|
+
list(accountId: string): Promise<ProviderIdentitySummary[]>;
|
|
41
|
+
/** Remove todas as identidades da conta; devolve quantas. */
|
|
42
|
+
unlinkAll(accountId: string): Promise<number>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { supportsProviderIdentity, } from './account_store.js';
|
|
2
|
+
/** O account store não tem account linking (o host não passou `providerIdentityModel`). */
|
|
3
|
+
export class ProviderIdentitiesUnsupportedError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super('[authkit] o accountStore não suporta identidades de provider — passe `providerIdentityModel` ao lucidAccountStore');
|
|
6
|
+
this.name = 'ProviderIdentitiesUnsupportedError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Identidades de provider (Google, GitHub, …) das contas: a MESMA ligação que o
|
|
11
|
+
* "Entrar com <provider>" consulta antes de cair no e-mail. Exposto como
|
|
12
|
+
* `authkit.identities` (`@adonis-agora/authkit-server/services/main`) para o host
|
|
13
|
+
* ligar uma conta a um provider fora do login social — ex.: quando a pessoa conecta
|
|
14
|
+
* o Google numa tela de integrações — sem tocar na tabela nem no account store cru.
|
|
15
|
+
*
|
|
16
|
+
* Store sem a capacidade: `supported` é `false` e os demais métodos lançam
|
|
17
|
+
* {@link ProviderIdentitiesUnsupportedError}. Nada falha em silêncio.
|
|
18
|
+
*/
|
|
19
|
+
export class ProviderIdentities {
|
|
20
|
+
#getStore;
|
|
21
|
+
constructor(getStore) {
|
|
22
|
+
this.#getStore = getStore;
|
|
23
|
+
}
|
|
24
|
+
/** O account store tem account linking por provider. */
|
|
25
|
+
get supported() {
|
|
26
|
+
return supportsProviderIdentity(this.#getStore());
|
|
27
|
+
}
|
|
28
|
+
#store() {
|
|
29
|
+
const store = this.#getStore();
|
|
30
|
+
if (!supportsProviderIdentity(store))
|
|
31
|
+
throw new ProviderIdentitiesUnsupportedError();
|
|
32
|
+
return store;
|
|
33
|
+
}
|
|
34
|
+
/** A conta ligada a `(provider, providerUserId)`; `null` se ninguém. */
|
|
35
|
+
find(provider, providerUserId) {
|
|
36
|
+
return this.#store().findByProviderIdentity(provider, providerUserId);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Liga a identidade à conta. Idempotente na chave `(provider, providerUserId)`: se
|
|
40
|
+
* ela já estava em OUTRA conta, MUDA para esta (e `previousAccountId` diz de onde).
|
|
41
|
+
* Só chame depois de provar que a pessoa é dona da identidade (OAuth concluído)
|
|
42
|
+
* E da conta (sessão nela).
|
|
43
|
+
*/
|
|
44
|
+
async link(input) {
|
|
45
|
+
const store = this.#store();
|
|
46
|
+
const previous = await store.findByProviderIdentity(input.provider, input.providerUserId);
|
|
47
|
+
await store.linkProviderIdentity(input);
|
|
48
|
+
return { previousAccountId: previous?.id ?? null };
|
|
49
|
+
}
|
|
50
|
+
/** As identidades da conta, sem tokens. */
|
|
51
|
+
list(accountId) {
|
|
52
|
+
return this.#store().listProviderIdentities(accountId);
|
|
53
|
+
}
|
|
54
|
+
/** Remove todas as identidades da conta; devolve quantas. */
|
|
55
|
+
unlinkAll(accountId) {
|
|
56
|
+
return this.#store().unlinkAllProviderIdentities(accountId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -122,6 +122,22 @@ export interface MailHooks {
|
|
|
122
122
|
agentName: string;
|
|
123
123
|
expiresInMinutes: number;
|
|
124
124
|
}) => Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Disparado num login bem-sucedido a partir de um IP NOVO para a conta (nenhum
|
|
127
|
+
* `login.success` anterior desse IP no audit sink), quando `notifications.newLoginEmail`
|
|
128
|
+
* está ligado. Best-effort, fire-and-forget: uma falha aqui NUNCA quebra o login.
|
|
129
|
+
* Quando ausente mas o mail estiver configurado, o host-kit envia o e-mail default de
|
|
130
|
+
* "novo login". O `login.new_ip_notified` é auditado nos dois casos.
|
|
131
|
+
*/
|
|
132
|
+
onNewLogin?: (data: {
|
|
133
|
+
account: {
|
|
134
|
+
id: string;
|
|
135
|
+
email: string;
|
|
136
|
+
};
|
|
137
|
+
ip: string;
|
|
138
|
+
userAgent?: string | null;
|
|
139
|
+
timestamp: string;
|
|
140
|
+
}) => Promise<void>;
|
|
125
141
|
/**
|
|
126
142
|
* Disparado num login bem-sucedido a partir de um dispositivo NOVO (sem cookie
|
|
127
143
|
* de dispositivo confiável válido para a conta). Best-effort, fire-and-forget:
|
|
@@ -51,7 +51,12 @@ export async function notifyLoginSuccess(ctx, cfg, input) {
|
|
|
51
51
|
resolvedEmail = (await cfg.accountStore.findById(accountId))?.email ?? null;
|
|
52
52
|
}
|
|
53
53
|
if (cfg.notifications.newLoginEmail) {
|
|
54
|
-
await maybeNotifyNewLogin(ctx, cfg, {
|
|
54
|
+
await maybeNotifyNewLogin(ctx, cfg, {
|
|
55
|
+
accountId,
|
|
56
|
+
email: resolvedEmail,
|
|
57
|
+
ip: ip ?? null,
|
|
58
|
+
userAgent,
|
|
59
|
+
});
|
|
55
60
|
}
|
|
56
61
|
if (onNewDevice && cfg.notifications.newDeviceEmail && resolvedEmail) {
|
|
57
62
|
await notifyNewDevice(ctx, cfg, {
|
|
@@ -108,12 +113,12 @@ async function notifyNewDevice(ctx, cfg, data) {
|
|
|
108
113
|
}
|
|
109
114
|
/**
|
|
110
115
|
* Verifica se o IP é novo para a conta (consultando o audit sink por
|
|
111
|
-
* `login.success` do subject) e, se for, envia o
|
|
112
|
-
* `login.new_ip_notified`. Degrada para no-op quando: sem IP, sem e-mail, sem
|
|
116
|
+
* `login.success` do subject) e, se for, envia o alerta (hook `mail.onNewLogin`,
|
|
117
|
+
* override do host, ou o mailer default) + audita `login.new_ip_notified`. Degrada para no-op quando: sem IP, sem e-mail, sem
|
|
113
118
|
* sink consultável (`list`), ou já houve um login.success deste IP antes.
|
|
114
119
|
*/
|
|
115
120
|
async function maybeNotifyNewLogin(ctx, cfg, data) {
|
|
116
|
-
const { accountId, email, ip } = data;
|
|
121
|
+
const { accountId, email, ip, userAgent } = data;
|
|
117
122
|
if (!ip || !email)
|
|
118
123
|
return;
|
|
119
124
|
// Sem consulta do histórico não dá pra decidir se o IP é novo → no-op.
|
|
@@ -133,7 +138,25 @@ async function maybeNotifyNewLogin(ctx, cfg, data) {
|
|
|
133
138
|
if (sameIpCount > 1)
|
|
134
139
|
return;
|
|
135
140
|
const when = new Date().toISOString();
|
|
136
|
-
|
|
141
|
+
if (cfg.mail?.onNewLogin) {
|
|
142
|
+
// Isolado: o mailer default engole os próprios erros, o hook do host não. Sem
|
|
143
|
+
// este try, um hook que lança cortaria o audit abaixo e o alerta de novo
|
|
144
|
+
// dispositivo, que roda depois deste na mesma tarefa.
|
|
145
|
+
try {
|
|
146
|
+
await cfg.mail.onNewLogin({
|
|
147
|
+
account: { id: accountId, email },
|
|
148
|
+
ip,
|
|
149
|
+
userAgent,
|
|
150
|
+
timestamp: when,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
ctx.logger?.error?.({ err: error, accountId }, 'authkit: falha no hook mail.onNewLogin');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await sendNewLoginEmail(ctx, cfg, { email, ip, when });
|
|
159
|
+
}
|
|
137
160
|
await cfg.audit?.record({
|
|
138
161
|
type: 'login.new_ip_notified',
|
|
139
162
|
accountId,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type MetricsRecorder } from '@adonis-agora/authkit-core';
|
|
2
|
+
import { ProviderIdentities } from '../accounts/provider_identities.js';
|
|
2
3
|
import type { ResolvedServerConfig } from '../define_config.js';
|
|
3
4
|
import { type ManagedKeyInfo } from '../keys/keystore.js';
|
|
4
5
|
import type { KeystoreManager } from '../keys/keystore_manager.js';
|
|
@@ -38,6 +39,12 @@ export declare class OidcService {
|
|
|
38
39
|
/** Holder mutável dos TTLs de access/id/refresh tokens (lido sincronamente pelo oidc-provider). */
|
|
39
40
|
readonly tokenTtlHolder: TokenTtlHolder;
|
|
40
41
|
get config(): ResolvedServerConfig;
|
|
42
|
+
/**
|
|
43
|
+
* Identidades de provider das contas — a ligação que o "Entrar com <provider>" usa.
|
|
44
|
+
* Para o host ligar uma conta a um provider fora do login social sem tocar no
|
|
45
|
+
* account store cru. Ver {@link ProviderIdentities}.
|
|
46
|
+
*/
|
|
47
|
+
get identities(): ProviderIdentities;
|
|
41
48
|
/** @internal Loader de JWKS do cofre (usado por reloadKeys). */
|
|
42
49
|
get jwksLoader(): (() => Promise<{
|
|
43
50
|
keys: Record<string, any>[];
|
|
@@ -2,6 +2,7 @@ import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
|
2
2
|
import { NoopRecorder } from '@adonis-agora/authkit-core';
|
|
3
3
|
import Koa from 'koa';
|
|
4
4
|
import mount from 'koa-mount';
|
|
5
|
+
import { ProviderIdentities } from '../accounts/provider_identities.js';
|
|
5
6
|
import { normalizeActiveOrg, readActiveOrgFromKoaCtx, verifyActiveOrgMembership, } from '../host/active_org_cookie.js';
|
|
6
7
|
import { isFirstPartyClient } from '../host/branding.js';
|
|
7
8
|
import { listKeyInfos, signingKeyAgeDays, toPublicJwks, } from '../keys/keystore.js';
|
|
@@ -66,6 +67,16 @@ export class OidcService {
|
|
|
66
67
|
get config() {
|
|
67
68
|
return this.#config;
|
|
68
69
|
}
|
|
70
|
+
#identities;
|
|
71
|
+
/**
|
|
72
|
+
* Identidades de provider das contas — a ligação que o "Entrar com <provider>" usa.
|
|
73
|
+
* Para o host ligar uma conta a um provider fora do login social sem tocar no
|
|
74
|
+
* account store cru. Ver {@link ProviderIdentities}.
|
|
75
|
+
*/
|
|
76
|
+
get identities() {
|
|
77
|
+
this.#identities ??= new ProviderIdentities(() => this.#config.accountStore);
|
|
78
|
+
return this.#identities;
|
|
79
|
+
}
|
|
69
80
|
/** @internal Loader de JWKS do cofre (usado por reloadKeys). */
|
|
70
81
|
get jwksLoader() {
|
|
71
82
|
return this.#deps.jwksLoader;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -117,8 +117,8 @@
|
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@adonis-agora/authkit-react": "0.26.0",
|
|
120
|
-
"@adonis-agora/durable": "0.
|
|
121
|
-
"@adonis-agora/telescope": "0.
|
|
120
|
+
"@adonis-agora/durable": "0.58.2",
|
|
121
|
+
"@adonis-agora/telescope": "0.27.0",
|
|
122
122
|
"@adonisjs/ally": "6.3.2",
|
|
123
123
|
"@adonisjs/auth": "10.1.0",
|
|
124
124
|
"@adonisjs/core": "7.6.0",
|