mirror of
https://github.com/OneUptime/oneuptime
synced 2024-11-21 22:59:07 +00:00
refactor: Update email regex to improve validation accuracy
This commit is contained in:
parent
766f1f6178
commit
f793f7dd16
@ -113,6 +113,7 @@
|
||||
"no-console": "error",
|
||||
"no-undef": "error",
|
||||
"no-empty": "error",
|
||||
"no-control-regex": "off",
|
||||
"prefer-arrow-callback": "error",
|
||||
"constructor-super": "error",
|
||||
"no-case-declarations": "error",
|
||||
|
@ -14,6 +14,7 @@ import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import { DASHBOARD_URL } from 'CommonUI/src/Config';
|
||||
import Alert, { AlertType } from 'CommonUI/src/Components/Alerts/Alert';
|
||||
import UiAnalytics from 'CommonUI/src/Utils/Analytics';
|
||||
import useAsyncEffect from 'use-async-effect';
|
||||
|
||||
const LoginPage: () => JSX.Element = () => {
|
||||
const apiUrl: URL = LOGIN_API_URL;
|
||||
@ -28,6 +29,16 @@ const LoginPage: () => JSX.Element = () => {
|
||||
|
||||
const [showSsoTip, setShowSSOTip] = useState<boolean>(false);
|
||||
|
||||
const [initialValues, setInitialValues] = React.useState<JSONObject>({});
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (Navigation.getQueryStringByName('email')) {
|
||||
setInitialValues({
|
||||
email: Navigation.getQueryStringByName('email'),
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="">
|
||||
@ -66,9 +77,13 @@ const LoginPage: () => JSX.Element = () => {
|
||||
field: {
|
||||
email: true,
|
||||
},
|
||||
title: 'Email',
|
||||
fieldType: FormFieldSchemaType.Email,
|
||||
placeholder: 'jeff@example.com',
|
||||
required: true,
|
||||
disabled: Boolean(
|
||||
initialValues && initialValues['email']
|
||||
),
|
||||
title: 'Email',
|
||||
dataTestId: 'email',
|
||||
},
|
||||
{
|
||||
@ -123,11 +138,13 @@ const LoginPage: () => JSX.Element = () => {
|
||||
|
||||
{showSsoTip && (
|
||||
<div className="text-gray-500 text-sm">
|
||||
Please sign in with your username
|
||||
and password. Once you have signed
|
||||
in, you'll be able to sign in
|
||||
via SSO that's configured for
|
||||
your project.
|
||||
Please log in from your SSO provider
|
||||
(like Okta / Entra ID, etc). We
|
||||
support login from the identity
|
||||
provider you have configured for
|
||||
your project. If you have not
|
||||
configured any SSO provider, please
|
||||
use the form above to log in.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -103,7 +103,8 @@ router.get(
|
||||
'/idp-login/:projectId/:projectSsoId',
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
return await loginUserWithSso(req, res);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/idp-login/:projectId/:projectSsoId',
|
||||
@ -112,12 +113,26 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
const loginUserWithSso = async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
type LoginUserWithSsoFunction = (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse
|
||||
) => Promise<void>;
|
||||
|
||||
debugger;
|
||||
const loginUserWithSso: LoginUserWithSsoFunction = async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const samlResponseBase64: string = req.body.SAMLResponse;
|
||||
|
||||
if (!samlResponseBase64) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('SAMLResponse not found')
|
||||
);
|
||||
}
|
||||
|
||||
const samlResponse: string = Buffer.from(
|
||||
samlResponseBase64,
|
||||
'base64'
|
||||
@ -146,8 +161,8 @@ const loginUserWithSso = async (req: ExpressRequest, res: ExpressResponse): Prom
|
||||
);
|
||||
}
|
||||
|
||||
const projectSSO: ProjectSSO | null =
|
||||
await ProjectSSOService.findOneBy({
|
||||
const projectSSO: ProjectSSO | null = await ProjectSSOService.findOneBy(
|
||||
{
|
||||
query: {
|
||||
projectId: new ObjectID(req.params['projectId']),
|
||||
_id: req.params['projectSsoId'],
|
||||
@ -164,7 +179,8 @@ const loginUserWithSso = async (req: ExpressRequest, res: ExpressResponse): Prom
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (!projectSSO) {
|
||||
return Response.sendErrorResponse(
|
||||
@ -226,15 +242,16 @@ const loginUserWithSso = async (req: ExpressRequest, res: ExpressResponse): Prom
|
||||
if (err instanceof Exception) {
|
||||
return Response.sendErrorResponse(req, res, err);
|
||||
}
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new ServerException()
|
||||
);
|
||||
return Response.sendErrorResponse(req, res, new ServerException());
|
||||
}
|
||||
|
||||
if (projectSSO.issuerURL.toString() !== issuerUrl) {
|
||||
logger.error("Issuer URL does not match. It should be "+projectSSO.issuerURL.toString()+" but it is "+issuerUrl.toString());
|
||||
logger.error(
|
||||
'Issuer URL does not match. It should be ' +
|
||||
projectSSO.issuerURL.toString() +
|
||||
' but it is ' +
|
||||
issuerUrl.toString()
|
||||
);
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
@ -266,18 +283,22 @@ const loginUserWithSso = async (req: ExpressRequest, res: ExpressResponse): Prom
|
||||
|
||||
/// Create a user.
|
||||
|
||||
alreadySavedUser = await UserService.createByEmail(email, {
|
||||
alreadySavedUser = await UserService.createByEmail({
|
||||
email,
|
||||
isEmailVerified: true,
|
||||
generateRandomPassword: true,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
isNewUser = true;
|
||||
}
|
||||
|
||||
// If he does not then add him to teams that he should belong and log in.
|
||||
// This should never happen because email is verified before he logs in with SSO.
|
||||
if (!alreadySavedUser.isEmailVerified && !isNewUser) {
|
||||
await AuthenticationEmail.sendVerificationEmail(
|
||||
alreadySavedUser!
|
||||
);
|
||||
await AuthenticationEmail.sendVerificationEmail(alreadySavedUser!);
|
||||
|
||||
return Response.render(
|
||||
req,
|
||||
@ -292,18 +313,17 @@ const loginUserWithSso = async (req: ExpressRequest, res: ExpressResponse): Prom
|
||||
}
|
||||
|
||||
// check if the user already belongs to the project
|
||||
const teamMemberCount: PositiveNumber =
|
||||
await TeamMemberService.countBy({
|
||||
const teamMemberCount: PositiveNumber = await TeamMemberService.countBy(
|
||||
{
|
||||
query: {
|
||||
projectId: new ObjectID(
|
||||
req.params['projectId'] as string
|
||||
),
|
||||
projectId: new ObjectID(req.params['projectId'] as string),
|
||||
userId: alreadySavedUser!.id!,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (teamMemberCount.toNumber() === 0) {
|
||||
// user not in project, add him to default teams.
|
||||
@ -343,21 +363,6 @@ const loginUserWithSso = async (req: ExpressRequest, res: ExpressResponse): Prom
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewUser) {
|
||||
return Response.render(
|
||||
req,
|
||||
res,
|
||||
'/usr/src/app/FeatureSet/Identity/Views/Message.ejs',
|
||||
{
|
||||
title: 'You have not signed up so far.',
|
||||
message:
|
||||
'You need to sign up for an account on OneUptime with this email:' +
|
||||
email.toString() +
|
||||
'. Once you have signed up, you can use SSO to log in to your project.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const projectId: ObjectID = new ObjectID(
|
||||
req.params['projectId'] as string
|
||||
);
|
||||
@ -378,20 +383,12 @@ const loginUserWithSso = async (req: ExpressRequest, res: ExpressResponse): Prom
|
||||
);
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol =
|
||||
await DatabaseConfig.getHttpProtocol();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserSSOKey(projectId),
|
||||
token,
|
||||
{
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(
|
||||
new PositiveNumber(30)
|
||||
),
|
||||
CookieUtil.setCookie(res, CookieUtil.getUserSSOKey(projectId), token, {
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
||||
httpOnly: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return Response.redirect(
|
||||
req,
|
||||
@ -409,7 +406,6 @@ const loginUserWithSso = async (req: ExpressRequest, res: ExpressResponse): Prom
|
||||
logger.error(err);
|
||||
Response.sendErrorResponse(req, res, new ServerException());
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default router;
|
||||
|
@ -68,7 +68,7 @@ export default class SSOUtil {
|
||||
const samlNameId: JSONArray =
|
||||
((samlSubject[0] as JSONObject)['saml2:NameID'] as JSONArray) ||
|
||||
((samlSubject[0] as JSONObject)['saml:NameID'] as JSONArray) ||
|
||||
((samlSubject[0] as JSONObject)['NameID'] as JSONArray)
|
||||
((samlSubject[0] as JSONObject)['NameID'] as JSONArray);
|
||||
|
||||
if (!samlNameId || samlNameId.length === 0) {
|
||||
throw new BadRequestException('SAML NAME ID not found');
|
||||
@ -126,12 +126,12 @@ export default class SSOUtil {
|
||||
payload =
|
||||
(payload['saml2p:Response'] as JSONObject) ||
|
||||
(payload['samlp:Response'] as JSONObject) ||
|
||||
(payload['Response'] as JSONObject)
|
||||
(payload['Response'] as JSONObject);
|
||||
|
||||
const samlAssertion: JSONArray =
|
||||
(payload['saml2:Assertion'] as JSONArray) ||
|
||||
(payload['saml:Assertion'] as JSONArray) ||
|
||||
(payload['Assertion'] as JSONArray)
|
||||
(payload['Assertion'] as JSONArray);
|
||||
|
||||
if (!samlAssertion || samlAssertion.length === 0) {
|
||||
throw new BadRequestException('SAML Assertion not found');
|
||||
@ -140,7 +140,7 @@ export default class SSOUtil {
|
||||
const samlSubject: JSONArray =
|
||||
((samlAssertion[0] as JSONObject)['saml2:Subject'] as JSONArray) ||
|
||||
((samlAssertion[0] as JSONObject)['saml:Subject'] as JSONArray) ||
|
||||
((samlAssertion[0] as JSONObject)['Subject'] as JSONArray)
|
||||
((samlAssertion[0] as JSONObject)['Subject'] as JSONArray);
|
||||
|
||||
if (!samlSubject || samlSubject.length === 0) {
|
||||
throw new BadRequestException('SAML Subject not found');
|
||||
@ -170,12 +170,12 @@ export default class SSOUtil {
|
||||
payload =
|
||||
(payload['saml2p:Response'] as JSONObject) ||
|
||||
(payload['samlp:Response'] as JSONObject) ||
|
||||
(payload['Response'] as JSONObject)
|
||||
(payload['Response'] as JSONObject);
|
||||
|
||||
const issuers: JSONArray =
|
||||
(payload['saml2:Issuer'] as JSONArray) ||
|
||||
(payload['saml:Issuer'] as JSONArray) ||
|
||||
(payload['Issuer'] as JSONArray)
|
||||
(payload['Issuer'] as JSONArray);
|
||||
|
||||
if (issuers.length === 0) {
|
||||
throw new BadRequestException('Issuers not found');
|
||||
|
@ -53,9 +53,10 @@ export default class Email extends DatabaseProperty {
|
||||
}
|
||||
|
||||
public static isValid(value: string): boolean {
|
||||
|
||||
// from https://datatracker.ietf.org/doc/html/rfc5322
|
||||
const re: RegExp = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/i;
|
||||
|
||||
const re: RegExp =
|
||||
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/i;
|
||||
const isValid: boolean = re.test(value);
|
||||
if (!isValid) {
|
||||
return false;
|
||||
|
@ -92,8 +92,11 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
|
||||
if (!user) {
|
||||
isNewUser = true;
|
||||
user = await UserService.createByEmail(email, {
|
||||
user = await UserService.createByEmail({
|
||||
email,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,8 @@ import UserNotificationSettingService from './UserNotificationSettingService';
|
||||
import Hostname from 'Common/Types/API/Hostname';
|
||||
import Protocol from 'Common/Types/API/Protocol';
|
||||
import { IsBillingEnabled } from '../EnvironmentConfig';
|
||||
import Text from 'Common/Types/Text';
|
||||
import HashedString from 'Common/Types/HashedString';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
@ -226,12 +228,21 @@ export class Service extends DatabaseService<Model> {
|
||||
return onUpdate;
|
||||
}
|
||||
|
||||
public async createByEmail(
|
||||
email: Email,
|
||||
props: DatabaseCommonInteractionProps
|
||||
): Promise<Model> {
|
||||
public async createByEmail(data: {
|
||||
email: Email;
|
||||
isEmailVerified?: boolean;
|
||||
generateRandomPassword?: boolean;
|
||||
props: DatabaseCommonInteractionProps;
|
||||
}): Promise<Model> {
|
||||
const { email, props } = data;
|
||||
|
||||
const user: Model = new Model();
|
||||
user.email = email;
|
||||
user.isEmailVerified = data.isEmailVerified || false;
|
||||
|
||||
if (data.generateRandomPassword) {
|
||||
user.password = new HashedString(Text.generateRandomText(20));
|
||||
}
|
||||
|
||||
if (!IsBillingEnabled) {
|
||||
// if billing is not enabled, then email is verified by default.
|
||||
|
Loading…
Reference in New Issue
Block a user