0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat: add support for nested attribute profile mapping (#6534)

* feat: add support for nested attribute profile mapping

* chore: undo version change

Undo version change since it’s handled by changeset

Co-authored-by: Darcy Ye <darcyye@silverhand.io>

* chore: remove new implementation and use essentials

Updated implementation to use essentials, throwing exception when not found

* fix: should use getSafe() and do not throw error when mapping profile

---------

Co-authored-by: Darcy Ye <darcyye@silverhand.io>
This commit is contained in:
DevTekVE 2024-09-05 11:59:52 +02:00 committed by GitHub
parent 459daeb4ac
commit 27d2c91d2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 94 additions and 14 deletions

View file

@ -0,0 +1,5 @@
---
"@logto/connector-oauth": minor
---
adding support for nested attributes

View file

@ -38,21 +38,43 @@ You are expected to find `authorizationEndpoint`, `tokenEndpoint` and `userInfoE
*userInfoEndpoint*: This endpoint is used by the client application to obtain additional information about the user, such as their fullname, email address or profile picture. The user info endpoint is typically accessed after the client application has obtained an access token from the token endpoint.
Logto also provide a `profileMap` field that users can customize the mapping from the social vendors' profiles which are usually not standard. The keys are Logto's standard user profile field names and corresponding values should be social profiles' field names. In current stage, Logto only concern 'id', 'name', 'avatar', 'email' and 'phone' from social profile, only 'id' is required and others are optional fields.
Logto also provides a `profileMap` field that users can customize the mapping from the social vendors' profiles which are usually not standard. The keys are Logto's standard user profile field names and corresponding values should be social profiles' field names. In the current stage, Logto only concerns 'id', 'name', 'avatar', 'email', and 'phone' from social profiles, only 'id' is required and others are optional fields.
`responseType` and `grantType` can ONLY be FIXED values with authorization code grant type, so we make them optional and default values will be automatically filled.
### Nested Attributes
For example, you can find [Google user profile response](https://developers.google.com/identity/openid-connect/openid-connect#an-id-tokens-payload) and hence its `profileMap` should be like:
The `profileMap` also supports nested attributes. You can map nested properties from the social vendor's profile to Logto's standard user profile fields.
For example, if the social vendor's profile has nested attributes like the following:
```json
{
"id": "sub",
"avatar": "picture"
"id": "123456",
"contact": {
"email": "octcat@github.com",
"phone": "123-456-7890"
},
"details": {
"name": "Oct Cat",
"avatar": {
"url": "avatar.png"
},
"groups": ["group1", "group2", "group3"]
}
}
```
You can configure the `profileMap` like this:
```json
{
"id": "id",
"name": "details.name",
"avatar": "details.avatar.url",
"email": "contact.email",
"phone": "contact.phone"
}
```
In this example, `details.name`, `details.avatar.url`, `contact.email`, and `contact.phone` are nested attributes in the social vendor's profile.
> **Note**
>
>
> We provided an OPTIONAL `customConfig` key to put your customize parameters.
> Each social identity provider could have their own variant on OAuth standard protocol. If your desired social identity provider strictly stick to OAuth standard protocol, the you do not need to care about `customConfig`.

View file

@ -48,4 +48,61 @@ describe('userProfileMapping', () => {
avatar: 'avatar.png',
});
});
it('should handle nested attributes correctly', () => {
const keyMapping: ProfileMap = {
id: 'id',
email: 'contact.email',
phone: 'contact.phone',
name: 'details.name',
avatar: 'details.avatar.url',
};
const originUserProfile = {
id: '123456',
contact: {
email: 'octcat@github.com',
phone: '123-456-7890',
},
details: {
name: 'Oct Cat',
avatar: {
url: 'avatar.png',
},
},
};
const profile: UserProfile = userProfileMapping(originUserProfile, keyMapping);
expect(profile).toMatchObject({
id: '123456',
email: 'octcat@github.com',
phone: '123-456-7890',
name: 'Oct Cat',
avatar: 'avatar.png',
});
});
it('should safely return undefined for non-existent nested attributes', () => {
const keyMapping: ProfileMap = {
id: 'id',
email: 'contact.email',
phone: 'contact.phone',
name: 'details.name',
avatar: 'details.avatar.url',
};
const originUserProfile = {
id: '123456',
contact: {
email: 'octcat@github.com',
},
details: {
name: 'Oct Cat',
},
};
const profile: UserProfile = userProfileMapping(originUserProfile, keyMapping);
expect(profile).toMatchObject({
id: '123456',
email: 'octcat@github.com',
name: 'Oct Cat',
});
});
});

View file

@ -1,4 +1,4 @@
import { assert } from '@silverhand/essentials';
import { assert, getSafe } from '@silverhand/essentials';
import { ConnectorError, ConnectorErrorCodes, parseJson } from '@logto/connector-kit';
import { type KyResponse } from 'ky';
@ -41,15 +41,11 @@ export const userProfileMapping = (
originUserProfile: object,
keyMapping: ProfileMap
) => {
const keyMap = new Map(
Object.entries(keyMapping).map(([destination, source]) => [source, destination])
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const mappedUserProfile = Object.fromEntries(
Object.entries(originUserProfile)
.filter(([key, value]) => keyMap.get(key) && value)
.map(([key, value]) => [keyMap.get(key), value])
Object.entries(keyMapping)
.map(([destination, source]) => [destination, getSafe(originUserProfile, source)])
.filter(([_, value]) => value)
);
const result = userProfileGuard.safeParse(mappedUserProfile);