Skip to content
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

Feat: enable dynamic profile field mapping via env variables for oidc response #1041

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
590e66c
feat: Enable dynamic profile field mapping via env variables
Junjiequan Jan 30, 2024
05fda06
Merge branch 'master' into SWAP-3745-scicat-be-make-oidc-user-validat…
Junjiequan Jan 30, 2024
a8c7150
add oidc groups to access groups
Junjiequan Jan 30, 2024
03fcc15
fix: minor fix for typo
Junjiequan Jan 30, 2024
f174ff3
fix: set default oidc displayName to userInfo.name
Junjiequan Jan 30, 2024
13739cf
Added removal of duplicates post-array concatenation
Junjiequan Jan 30, 2024
7458961
fixed typo and wrong user profile schema
Junjiequan Jan 30, 2024
854cf3f
Merge branch 'master' into SWAP-3745-scicat-be-make-oidc-user-validat…
Junjiequan Jan 30, 2024
4864519
Allow for override of configuration
bpedersen2 Sep 11, 2023
b07606e
Make the acccess groups service configurable
bpedersen2 Sep 11, 2023
c17335c
Add example graphql handler
bpedersen2 Nov 23, 2023
ca4f17c
removed redundant code
Junjiequan Jan 31, 2024
b96d585
OIDC userinfo and user query settings have been made configurable
Junjiequan Feb 5, 2024
927525a
improved if condition for parseQueryFilter
Junjiequan Feb 6, 2024
6b9cf7f
fixed wrong default logger method integration
Junjiequan Feb 6, 2024
2736ea3
improved logger message format
Junjiequan Feb 7, 2024
2f3c50d
get accessGroupsProperty from userPayload for AccesGroupFromPayloadSe…
Junjiequan Feb 7, 2024
75cf22a
fix access-group-from-payload unit test fail
Junjiequan Feb 7, 2024
13c4f69
refactor: improved readability of parseQueryFilter in oidc.strategy file
Junjiequan Feb 7, 2024
29e3e3d
fixed defaultLogger to log message without undefined even second para…
Junjiequan Feb 7, 2024
952621a
Merge branch 'master' into SWAP-3745-scicat-be-make-oidc-user-validat…
nitrosx Feb 9, 2024
f26522f
moved externalId and provider of create-user-identity dto to update-u…
Junjiequan Feb 9, 2024
f3b7268
Merge branch 'master' into SWAP-3745-scicat-be-make-oidc-user-validat…
Junjiequan Feb 13, 2024
0b701c1
Merge branch 'master' into SWAP-3745-scicat-be-make-oidc-user-validat…
Junjiequan Feb 20, 2024
c300a77
minor refactoring
Junjiequan Feb 22, 2024
fd7e9c1
README updates for new environment variables
Junjiequan Feb 22, 2024
5d51633
Merge branch 'master' into SWAP-3745-scicat-be-make-oidc-user-validat…
Junjiequan Feb 22, 2024
571cfbd
Merge branch 'master' into SWAP-3745-scicat-be-make-oidc-user-validat…
Junjiequan Feb 26, 2024
2f52cf7
Update README.md
Junjiequan Feb 27, 2024
24cf7a3
Merge branch 'master' into SWAP-3745-scicat-be-make-oidc-user-validat…
Junjiequan Feb 28, 2024
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
75 changes: 56 additions & 19 deletions src/auth/strategies/oidc.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
import { OidcConfig } from "src/config/configuration";
import { AccessGroupService } from "../access-group-provider/access-group.service";
import { UserPayload } from "../interfaces/userPayload.interface";
import { IUserInfoMapping } from "src/common/interfaces/common.interface";

type UserInfoResPonseWithGroups = UserinfoResponse & {
Junjiequan marked this conversation as resolved.
Show resolved Hide resolved
groups: string[];
};

export class BuildOpenIdClient {
constructor(private configService: ConfigService) {}
Expand Down Expand Up @@ -66,7 +71,8 @@
}

async validate(tokenset: TokenSet): Promise<Omit<User, "password">> {
const userinfo: UserinfoResponse = await this.client.userinfo(tokenset);
const userinfo: UserInfoResPonseWithGroups =
await this.client.userinfo(tokenset);
const oidcConfig = this.configService.get<OidcConfig>("oidc");

const userProfile = this.parseUserInfo(userinfo);
Expand All @@ -77,12 +83,19 @@
accessGroupProperty: oidcConfig?.accessGroupProperty,
payload: userinfo,
};
userProfile.accessGroups =
const accessGroups =
await this.accessGroupService.getAccessGroups(userPayload);

if (userProfile.groups && userProfile.groups.length > 0) {
userProfile.accessGroups = [...accessGroups, ...userProfile.groups];
} else {
userProfile.accessGroups = accessGroups;
}
nitrosx marked this conversation as resolved.
Show resolved Hide resolved
delete userProfile.groups;

const userFilter: FilterQuery<UserDocument> = {
$or: [
{ username: `oidc.${userProfile.username}` },
bpedersen2 marked this conversation as resolved.
Show resolved Hide resolved
{ username: userProfile.username },
{ email: userProfile.email as string },
],
};
Expand Down Expand Up @@ -119,46 +132,70 @@
await this.usersService.updateUserIdentity(
{
profile: userProfile,
externalId: userProfile.id,
},
user._id,
);
}

const jsonUser = JSON.parse(JSON.stringify(user));
const { password, ...returnUser } = jsonUser;

Check warning on line 142 in src/auth/strategies/oidc.strategy.ts

View workflow job for this annotation

GitHub Actions / eslint

'password' is assigned a value but never used
returnUser.userId = returnUser._id;

return returnUser;
}

getUserPhoto(userinfo: UserinfoResponse) {
return userinfo.thumbnailPhoto
getUserPhoto(thumbnailPhoto: string) {
return thumbnailPhoto
? "data:image/jpeg;base64," +
Buffer.from(userinfo.thumbnailPhoto as string, "binary").toString(
"base64",
)
Buffer.from(thumbnailPhoto, "binary").toString("base64")
: "no photo";
}

parseUserInfo(userinfo: UserinfoResponse) {
parseUserInfo(userinfo: UserInfoResPonseWithGroups) {
type OidcProfile = Profile & UserProfile;
const profile = {} as OidcProfile;

const newUserInfoFields =
this.configService.get<IUserInfoMapping>("oidc.userInfoMapping") || {};

// To dynamically map user info fields based on environment variables,
// set mappings like OIDC_USERINFO_MAPPING_FIELD_USERNAME=family_name.
// This assigns userinfo.family_name to oidcUser.username.

const oidcUser: IUserInfoMapping = {
id: userinfo["sub"] || (userinfo["user_id"] as string) || "",
username: userinfo["sub"] || "",
displayName: userinfo["name"] || "",
familyName: userinfo["family_name"] || "",
email: userinfo["email"] || "",
thumbnailPhoto: (userinfo["thumbnailPhoto"] as string) || "",
nitrosx marked this conversation as resolved.
Show resolved Hide resolved
groups: userinfo["groups"] || [],
};

Object.entries(newUserInfoFields).forEach(([sourceField, targetField]) => {
if (
typeof targetField === "string" &&
oidcUser.hasOwnProperty(sourceField)
) {
oidcUser[sourceField] = userinfo[targetField] as string;
}
});

// Prior to OpenID Connect Basic Client Profile 1.0 - draft 22, the "sub"
// claim was named "user_id". Many providers still use the old name, so
// fallback to that.
const userId = userinfo.sub || (userinfo.user_id as string);
if (!userId) {
// fallback to that. https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims

if (!oidcUser.id) {
throw new Error("Could not find sub or user_id in userinfo response");
}

profile.id = userId;
profile.username = userinfo.preferred_username ?? userinfo.name ?? "";
profile.displayName = userinfo.name ?? "";
profile.emails = userinfo.email ? [{ value: userinfo.email }] : [];
profile.email = userinfo.email ?? "";
profile.thumbnailPhoto = this.getUserPhoto(userinfo);
profile.displayName = oidcUser.displayName + oidcUser.familyName;
profile.emails = oidcUser.email ? [{ value: oidcUser.email }] : [];
profile.thumbnailPhoto = this.getUserPhoto(oidcUser.thumbnailPhoto);

const oidcUserProfile = { ...oidcUser, ...profile };

return profile;
return oidcUserProfile;
}
}
11 changes: 11 additions & 0 deletions src/common/interfaces/common.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,14 @@ export interface IDatafileFilter {
gid?: string;
perm?: string;
}

export interface IUserInfoMapping {
id: string;
username: string;
displayName: string;
familyName: string;
email: string;
thumbnailPhoto: string;
groups?: string[];
[key: string]: string | string[] | undefined;
}
10 changes: 10 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ const configuration = () => {
accessGroupProperty: process.env.OIDC_ACCESS_GROUPS_PROPERTY, // Example: groups
autoLogout: process.env.OIDC_AUTO_LOGOUT || false,
returnURL: process.env.OIDC_RETURN_URL,
userInfoMapping: {
id: process.env.OIDC_USERINFO_MAPPING_FIELD_ID,
username: process.env.OIDC_USERINFO_MAPPING_FIELD_USERNAME,
displayName: process.env.OIDC_USERINFO_MAPPING_FIELD_DISPLAYNAME,
familyName: process.env.OIDC_USERINFO_MAPPING_FIELD_FAMILYNAME,
emails: process.env.OIDC_USERINFO_MAPPING_FIELD_EMAILS,
email: process.env.OIDC_USERINFO_MAPPING_FIELD_EMAIL,
thumbnailPhoto: process.env.OIDC_USERINFO_MAPPING_FIELD_THUMBNAIL_PHOTO,
groups: process.env.OIDC_USERINFO_MAPPING_FIELD_GROUPS,
nitrosx marked this conversation as resolved.
Show resolved Hide resolved
},
},
logbook: {
enabled:
Expand Down
3 changes: 3 additions & 0 deletions src/users/schemas/user-profile.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export class UserProfile {

@Prop({ type: [String] })
accessGroups: string[];

@Prop()
groups?: string[];
Junjiequan marked this conversation as resolved.
Show resolved Hide resolved
}

export const UserProfileSchema = SchemaFactory.createForClass(UserProfile);
Loading