Skip to content

feat(core,schemas): add get user social identities token secret API #7494

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions packages/core/src/queries/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@
type SecretSocialConnectorRelationPayload,
SecretSocialConnectorRelations,
SecretType,
type SocialTokenSetSecret,
} from '@logto/schemas';
import { sql, type CommonQueryMethods } from '@silverhand/slonik';

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

const secrets = convertToIdentifiers(Secrets);
const secretSocialConnectorRelations = convertToIdentifiers(SecretSocialConnectorRelations);
const secrets = convertToIdentifiers(Secrets, true);
const secretSocialConnectorRelations = convertToIdentifiers(SecretSocialConnectorRelations, true);

class SecretQueries extends SchemaQueries<SecretKeys, CreateSecret, Secret> {
constructor(pool: CommonQueryMethods) {
Expand Down Expand Up @@ -61,6 +62,24 @@
});
});
}

/**
* For user federated token exchange use, we need to find the secret by user id and social target.
*/
public async findSocialTokenSetSecretByUserIdAndTarget(userId: string, target: string) {
return this.pool.maybeOne<SocialTokenSetSecret>(sql`
select ${sql.join(Object.values(secrets.fields), sql`, `)},
${secretSocialConnectorRelations.fields.connectorId},
${secretSocialConnectorRelations.fields.identityId},
${secretSocialConnectorRelations.fields.target}
from ${secrets.table}
join ${secretSocialConnectorRelations.table}
on ${secrets.fields.id} = ${secretSocialConnectorRelations.fields.secretId}
where ${secrets.fields.userId} = ${userId}
and ${secrets.fields.type} = ${SecretType.FederatedTokenSet}
and ${secretSocialConnectorRelations.fields.target} = ${target}
`);
}

Check warning on line 82 in packages/core/src/queries/secret.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/queries/secret.ts#L70-L82

Added lines #L70 - L82 were not covered by tests
}

export default SecretQueries;
16 changes: 16 additions & 0 deletions packages/core/src/routes/admin-user/social.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@
}
}
}
},
"/api/users/{userId}/identities/{target}/secret": {
"get": {
"tags": ["Dev feature"],
"parameters": [],
"responses": {
"200": {
"description": "Token set record of the social identity. (For security reasons, encrypted token details will be omitted from the response.)"
},
"404": {
"description": "User social identity not found, or no token set record exists for the user's social identity."
}
},
"summary": "Retrieve the social provider token set for the current user.",
"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. "
}
}
}
}
39 changes: 39 additions & 0 deletions packages/core/src/routes/admin-user/social.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
identityGuard,
identitiesGuard,
userProfileResponseGuard,
desensitizedSocialTokenSetSecretGuard,
} from '@logto/schemas';
import { has } from '@silverhand/essentials';
import { object, record, string, unknown } from 'zod';
Expand All @@ -12,6 +13,7 @@
import koaGuard from '#src/middleware/koa-guard.js';
import assertThat from '#src/utils/assert-that.js';

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

Expand All @@ -21,6 +23,7 @@
const {
queries: {
users: { findUserById, updateUserById, hasUserWithIdentity, deleteUserIdentity },
secrets: secretQueries,
},
connectors: { getLogtoConnectorById },
} = tenant;
Expand Down Expand Up @@ -154,4 +157,40 @@
return next();
}
);

if (EnvSet.values.isDevFeaturesEnabled) {
router.get(
'/users/:userId/identities/:target/secret',
koaGuard({
params: object({ userId: string(), target: string() }),
response: desensitizedSocialTokenSetSecretGuard,
status: [200, 404],
}),
async (ctx, next) => {
const {
params: { userId, target },
} = ctx.guard;

const { identities } = await findUserById(userId);
if (!has(identities, target)) {
throw new RequestError({ code: 'user.identity_not_exist', status: 404 });
}

const secret = await secretQueries.findSocialTokenSetSecretByUserIdAndTarget(
userId,
target
);

if (!secret) {
throw new RequestError({ code: 'entity.not_found', status: 404 });
}

const { encryptedDek, iv, authTag, ciphertext, ...rest } = secret;

ctx.body = rest;

return next();
}

Check warning on line 193 in packages/core/src/routes/admin-user/social.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/admin-user/social.ts#L170-L193

Added lines #L170 - L193 were not covered by tests
);
}
}
1 change: 1 addition & 0 deletions packages/core/src/routes/connector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
}

if (enableTokenStorage) {
// TODO: remove this check once the feature is enabled in production.

Check warning on line 141 in packages/core/src/routes/connector/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

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

[no-warning-comments] Unexpected 'todo' comment: 'TODO: remove this check once the feature...'.
assertThat(
EnvSet.values.isDevFeaturesEnabled,
new RequestError('request.feature_not_supported')
Expand Down Expand Up @@ -289,7 +289,7 @@
}

if (enableTokenStorage) {
// TODO: remove this check once the feature is enabled in production.

Check warning on line 292 in packages/core/src/routes/connector/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

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

[no-warning-comments] Unexpected 'todo' comment: 'TODO: remove this check once the feature...'.
assertThat(
EnvSet.values.isDevFeaturesEnabled,
new RequestError('request.feature_not_supported')
Expand Down Expand Up @@ -319,6 +319,7 @@
config: conditional(config && (cleanDeep(config) as JsonObject)),
metadata: conditional(metadata && cleanDeep(metadata)),
syncProfile,
enableTokenStorage,
},
where: { id },
jsonbMode: 'replace',
Expand Down
28 changes: 28 additions & 0 deletions packages/schemas/src/types/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from 'zod';

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

export const encryptedSecretGuard = Secrets.guard.pick({
encryptedDek: true,
Expand Down Expand Up @@ -49,3 +50,30 @@ export const secretSocialConnectorRelationPayloadGuard =
export type SecretSocialConnectorRelationPayload = z.infer<
typeof secretSocialConnectorRelationPayloadGuard
>;

export const socialTokenSetSecretGuard = Secrets.guard.extend({
type: z.literal(SecretType.FederatedTokenSet),
metadata: socialConnectorTokenSetMetadataGuard,
connectorId: z.string(),
identityId: z.string(),
target: z.string(),
});

/**
* Social token set secret type
* - Secret type is `FederatedTokenSet`
* - Metadata is the social connector token set metadata
* - Joined with the social connector relation
*/
export type SocialTokenSetSecret = z.infer<typeof socialTokenSetSecretGuard>;

export const desensitizedSocialTokenSetSecretGuard = socialTokenSetSecretGuard.omit({
encryptedDek: true,
iv: true,
authTag: true,
ciphertext: true,
});

export type DesensitizedSocialTokenSetSecret = z.infer<
typeof desensitizedSocialTokenSetSecretGuard
>;
Loading