Skip to content

Commit 1597dd0

Browse files
authored
feat(core,schemas): add get user social identities token secret API (#7494)
* feat(core,schemas): add get user social identities token secret API add get users social identities token secret API * feat(core): add api docs add api docs * fix(core): fix patch connector API fix patch connector API enableTokenStorage not updated bug
1 parent 932ce54 commit 1597dd0

File tree

5 files changed

+105
-2
lines changed

5 files changed

+105
-2
lines changed

packages/core/src/queries/secret.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import {
77
type SecretSocialConnectorRelationPayload,
88
SecretSocialConnectorRelations,
99
SecretType,
10+
type SocialTokenSetSecret,
1011
} from '@logto/schemas';
1112
import { sql, type CommonQueryMethods } from '@silverhand/slonik';
1213

1314
import { buildInsertIntoWithPool } from '../database/insert-into.js';
1415
import SchemaQueries from '../utils/SchemaQueries.js';
1516
import { convertToIdentifiers } from '../utils/sql.js';
1617

17-
const secrets = convertToIdentifiers(Secrets);
18-
const secretSocialConnectorRelations = convertToIdentifiers(SecretSocialConnectorRelations);
18+
const secrets = convertToIdentifiers(Secrets, true);
19+
const secretSocialConnectorRelations = convertToIdentifiers(SecretSocialConnectorRelations, true);
1920

2021
class SecretQueries extends SchemaQueries<SecretKeys, CreateSecret, Secret> {
2122
constructor(pool: CommonQueryMethods) {
@@ -61,6 +62,24 @@ class SecretQueries extends SchemaQueries<SecretKeys, CreateSecret, Secret> {
6162
});
6263
});
6364
}
65+
66+
/**
67+
* For user federated token exchange use, we need to find the secret by user id and social target.
68+
*/
69+
public async findSocialTokenSetSecretByUserIdAndTarget(userId: string, target: string) {
70+
return this.pool.maybeOne<SocialTokenSetSecret>(sql`
71+
select ${sql.join(Object.values(secrets.fields), sql`, `)},
72+
${secretSocialConnectorRelations.fields.connectorId},
73+
${secretSocialConnectorRelations.fields.identityId},
74+
${secretSocialConnectorRelations.fields.target}
75+
from ${secrets.table}
76+
join ${secretSocialConnectorRelations.table}
77+
on ${secrets.fields.id} = ${secretSocialConnectorRelations.fields.secretId}
78+
where ${secrets.fields.userId} = ${userId}
79+
and ${secrets.fields.type} = ${SecretType.FederatedTokenSet}
80+
and ${secretSocialConnectorRelations.fields.target} = ${target}
81+
`);
82+
}
6483
}
6584

6685
export default SecretQueries;

packages/core/src/routes/admin-user/social.openapi.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@
6868
}
6969
}
7070
}
71+
},
72+
"/api/users/{userId}/identities/{target}/secret": {
73+
"get": {
74+
"tags": ["Dev feature"],
75+
"parameters": [],
76+
"responses": {
77+
"200": {
78+
"description": "Token set record of the social identity. (For security reasons, encrypted token details will be omitted from the response.)"
79+
},
80+
"404": {
81+
"description": "User social identity not found, or no token set record exists for the user's social identity."
82+
}
83+
},
84+
"summary": "Retrieve the social provider token set for the current user.",
85+
"description": "This API retrieves the token set record associated with the current user's social identity from the Logto Secret Vault. The operation is only available if the corresponding social connector has token set storage enabled. If a token set record exists for the user's social identity, it will be returned. "
86+
}
7187
}
7288
}
7389
}

packages/core/src/routes/admin-user/social.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
identityGuard,
55
identitiesGuard,
66
userProfileResponseGuard,
7+
desensitizedSocialTokenSetSecretGuard,
78
} from '@logto/schemas';
89
import { has } from '@silverhand/essentials';
910
import { object, record, string, unknown } from 'zod';
@@ -12,6 +13,7 @@ import RequestError from '#src/errors/RequestError/index.js';
1213
import koaGuard from '#src/middleware/koa-guard.js';
1314
import assertThat from '#src/utils/assert-that.js';
1415

16+
import { EnvSet } from '../../env-set/index.js';
1517
import { transpileUserProfileResponse } from '../../utils/user.js';
1618
import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
1719

@@ -21,6 +23,7 @@ export default function adminUserSocialRoutes<T extends ManagementApiRouter>(
2123
const {
2224
queries: {
2325
users: { findUserById, updateUserById, hasUserWithIdentity, deleteUserIdentity },
26+
secrets: secretQueries,
2427
},
2528
connectors: { getLogtoConnectorById },
2629
} = tenant;
@@ -154,4 +157,40 @@ export default function adminUserSocialRoutes<T extends ManagementApiRouter>(
154157
return next();
155158
}
156159
);
160+
161+
if (EnvSet.values.isDevFeaturesEnabled) {
162+
router.get(
163+
'/users/:userId/identities/:target/secret',
164+
koaGuard({
165+
params: object({ userId: string(), target: string() }),
166+
response: desensitizedSocialTokenSetSecretGuard,
167+
status: [200, 404],
168+
}),
169+
async (ctx, next) => {
170+
const {
171+
params: { userId, target },
172+
} = ctx.guard;
173+
174+
const { identities } = await findUserById(userId);
175+
if (!has(identities, target)) {
176+
throw new RequestError({ code: 'user.identity_not_exist', status: 404 });
177+
}
178+
179+
const secret = await secretQueries.findSocialTokenSetSecretByUserIdAndTarget(
180+
userId,
181+
target
182+
);
183+
184+
if (!secret) {
185+
throw new RequestError({ code: 'entity.not_found', status: 404 });
186+
}
187+
188+
const { encryptedDek, iv, authTag, ciphertext, ...rest } = secret;
189+
190+
ctx.body = rest;
191+
192+
return next();
193+
}
194+
);
195+
}
157196
}

packages/core/src/routes/connector/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ export default function connectorRoutes<T extends ManagementApiRouter>(
319319
config: conditional(config && (cleanDeep(config) as JsonObject)),
320320
metadata: conditional(metadata && cleanDeep(metadata)),
321321
syncProfile,
322+
enableTokenStorage,
322323
},
323324
where: { id },
324325
jsonbMode: 'replace',

packages/schemas/src/types/secrets.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { z } from 'zod';
22

33
import { SecretSocialConnectorRelations } from '../db-entries/secret-social-connector-relation.js';
44
import { type CreateSecret, Secrets } from '../db-entries/secret.js';
5+
import { SecretType } from '../foundations/index.js';
56

67
export const encryptedSecretGuard = Secrets.guard.pick({
78
encryptedDek: true,
@@ -49,3 +50,30 @@ export const secretSocialConnectorRelationPayloadGuard =
4950
export type SecretSocialConnectorRelationPayload = z.infer<
5051
typeof secretSocialConnectorRelationPayloadGuard
5152
>;
53+
54+
export const socialTokenSetSecretGuard = Secrets.guard.extend({
55+
type: z.literal(SecretType.FederatedTokenSet),
56+
metadata: socialConnectorTokenSetMetadataGuard,
57+
connectorId: z.string(),
58+
identityId: z.string(),
59+
target: z.string(),
60+
});
61+
62+
/**
63+
* Social token set secret type
64+
* - Secret type is `FederatedTokenSet`
65+
* - Metadata is the social connector token set metadata
66+
* - Joined with the social connector relation
67+
*/
68+
export type SocialTokenSetSecret = z.infer<typeof socialTokenSetSecretGuard>;
69+
70+
export const desensitizedSocialTokenSetSecretGuard = socialTokenSetSecretGuard.omit({
71+
encryptedDek: true,
72+
iv: true,
73+
authTag: true,
74+
ciphertext: true,
75+
});
76+
77+
export type DesensitizedSocialTokenSetSecret = z.infer<
78+
typeof desensitizedSocialTokenSetSecretGuard
79+
>;

0 commit comments

Comments
 (0)