fix(auth/oidc): string "undefined" for missing userinfo response fields

The userinfo response endpoint from the OIDC provider should not be
trusted to return what we expect. Fields could be undefined. In that
case HedgeDoc would have written "undefined" into the fields for
profile picture or email address.
This fix checks for fields being undefined and returns a default value
in that case.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2024-09-13 16:29:26 +02:00 committed by Philip Molares
parent e8793271a0
commit c4c5cbd5d0

View file

@ -9,7 +9,7 @@ import {
InternalServerErrorException, InternalServerErrorException,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { Client, generators, Issuer } from 'openid-client'; import { Client, generators, Issuer, UserinfoResponse } from 'openid-client';
import appConfiguration, { AppConfig } from '../../config/app.config'; import appConfiguration, { AppConfig } from '../../config/app.config';
import authConfiguration, { import authConfiguration, {
@ -178,16 +178,31 @@ export class OidcService {
request.session.oidcIdToken = tokenSet.id_token; request.session.oidcIdToken = tokenSet.id_token;
const userInfoResponse = await client.userinfo(tokenSet); const userInfoResponse = await client.userinfo(tokenSet);
const userId = String( const userId = OidcService.getResponseFieldValue(
userInfoResponse[oidcConfig.userIdField] || userInfoResponse.sub, userInfoResponse,
oidcConfig.userIdField,
userInfoResponse.sub,
); );
const username = String( const username = OidcService.getResponseFieldValue(
userInfoResponse[oidcConfig.userNameField] || userInfoResponse,
userInfoResponse[oidcConfig.userIdField], oidcConfig.userNameField,
userId,
).toLowerCase() as Lowercase<string>; ).toLowerCase() as Lowercase<string>;
const displayName = String(userInfoResponse[oidcConfig.displayNameField]); const displayName = OidcService.getResponseFieldValue(
const email = String(userInfoResponse[oidcConfig.emailField]); userInfoResponse,
const photoUrl = String(userInfoResponse[oidcConfig.profilePictureField]); oidcConfig.displayNameField,
username,
);
const email = OidcService.getResponseFieldValue(
userInfoResponse,
oidcConfig.emailField,
undefined,
);
const photoUrl = OidcService.getResponseFieldValue(
userInfoResponse,
oidcConfig.profilePictureField,
undefined,
);
const newUserData = { const newUserData = {
username, username,
displayName, displayName,
@ -261,4 +276,20 @@ export class OidcService {
} }
return `${endSessionEndpoint}?post_logout_redirect_uri=${this.appConfig.baseUrl}${idToken ? `&id_token_hint=${idToken}` : ''}`; return `${endSessionEndpoint}?post_logout_redirect_uri=${this.appConfig.baseUrl}${idToken ? `&id_token_hint=${idToken}` : ''}`;
} }
/**
* Returns a specific field from the userinfo object or a default value.
*
* @param {UserinfoResponse} response The response from the OIDC userinfo endpoint
* @param {string} field The field to get from the response
* @param {string|undefined} defaultValue The default value to return if the value is empty
* @returns {string|undefined} The value of the field from the response or the default value
*/
private static getResponseFieldValue<T extends string | undefined>(
response: UserinfoResponse,
field: string,
defaultValue: T,
): string | T {
return response[field] ? String(response[field]) : defaultValue;
}
} }