Skip to content
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
30 changes: 27 additions & 3 deletions api/src/Feature.Auth/ForgotPassword/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,24 @@
public override async Task<Results<Ok, ProblemHttpResult>> ExecuteAsync(Request request, CancellationToken ct)
{
var user = await userManager.FindByEmailAsync(request.Email.Trim());
if (user is null || !await userManager.IsEmailConfirmedAsync(user))
if (user is null)
{
logger.LogWarning("Possible user enumeration. Unknown email received {email}", request.Email);
// Don't reveal that the user does not exist or is not confirmed
return TypedResults.Ok();
}

var mail = await userManager.IsEmailConfirmedAsync(user)
? await GetResetPasswordEmail(user)
: GetAcceptInviteEmail(user);

jobService.EnqueueSendEmail(request.Email, mail.Subject, mail.Body);

return TypedResults.Ok();
}

private async Task<EmailModel> GetResetPasswordEmail(ApplicationUser user)
{
// For more information on how to enable account confirmation and password reset please
// visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await userManager.GeneratePasswordResetTokenAsync(user);
Expand All @@ -45,8 +56,21 @@
var emailProps = new ResetPasswordEmailProps(FullName: user.DisplayName, CdnUrl: _apiConfig.WebAppUrl, ResetPasswordUrl: passwordResetUrl);
var mail = emailFactory.GenerateResetPasswordEmail(emailProps);

jobService.EnqueueSendEmail(request.Email, mail.Subject, mail.Body);
return mail;
}

return TypedResults.Ok();
private EmailModel GetAcceptInviteEmail(ApplicationUser user)
{
var endpointUri = new Uri(Path.Combine($"{_apiConfig.WebAppUrl}", "accept-invite"));

Check notice

Code scanning / CodeQL

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.

Copilot Autofix

AI 11 months ago

To fix the issue, replace the use of Path.Combine with a method that is more suitable for combining URLs. In this case, simple string concatenation with a forward slash (/) is appropriate because URLs use / as the path separator. Ensure that _apiConfig.WebAppUrl does not end with a trailing slash to avoid double slashes in the resulting URL.

The changes will be made in the GetAcceptInviteEmail method, specifically on line 64. A similar issue exists in the GetResetPasswordEmail method on line 53, so it will also be addressed for consistency and correctness.


Suggested changeset 1
api/src/Feature.Auth/ForgotPassword/Endpoint.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/Feature.Auth/ForgotPassword/Endpoint.cs b/api/src/Feature.Auth/ForgotPassword/Endpoint.cs
--- a/api/src/Feature.Auth/ForgotPassword/Endpoint.cs
+++ b/api/src/Feature.Auth/ForgotPassword/Endpoint.cs
@@ -52,3 +52,3 @@
         
-        var endpointUri = new Uri(Path.Combine($"{_apiConfig.WebAppUrl}", "reset-password"));
+        var endpointUri = new Uri($"{_apiConfig.WebAppUrl.TrimEnd('/')}/reset-password");
         string passwordResetUrl = QueryHelpers.AddQueryString(endpointUri.ToString(), "token", code);
@@ -63,3 +63,3 @@
     {
-        var endpointUri = new Uri(Path.Combine($"{_apiConfig.WebAppUrl}", "accept-invite"));
+        var endpointUri = new Uri($"{_apiConfig.WebAppUrl.TrimEnd('/')}/accept-invite");
         string acceptInviteUrl =
EOF
@@ -52,3 +52,3 @@

var endpointUri = new Uri(Path.Combine($"{_apiConfig.WebAppUrl}", "reset-password"));
var endpointUri = new Uri($"{_apiConfig.WebAppUrl.TrimEnd('/')}/reset-password");
string passwordResetUrl = QueryHelpers.AddQueryString(endpointUri.ToString(), "token", code);
@@ -63,3 +63,3 @@
{
var endpointUri = new Uri(Path.Combine($"{_apiConfig.WebAppUrl}", "accept-invite"));
var endpointUri = new Uri($"{_apiConfig.WebAppUrl.TrimEnd('/')}/accept-invite");
string acceptInviteUrl =
Copilot is powered by AI and may make mistakes. Always verify output.
string acceptInviteUrl =
QueryHelpers.AddQueryString(endpointUri.ToString(), "invitationToken", user.InvitationToken);

var confirmEmailProps = new ConfirmEmailProps(
CdnUrl: _apiConfig.WebAppUrl,
FullName: user.DisplayName,
ConfirmUrl: acceptInviteUrl);

var mail = emailFactory.GenerateConfirmAccountEmail(confirmEmailProps);
return mail;
}
}
13 changes: 7 additions & 6 deletions web/src/components/ui/password-input.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { EyeIcon, EyeOffIcon } from 'lucide-react';
import * as React from 'react';

import { Button } from '@/components/ui/button';
import { Input, InputProps } from '@/components/ui/input';
import { Input, type InputProps } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { EyeIcon, EyeOffIcon } from 'lucide-react';
import { forwardRef, useState } from 'react';

const PasswordInput = forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
const [showPassword, setShowPassword] = useState(false);
const disabled = props['value'] === '' || props['value'] === undefined || props['disabled'];
const PasswordInput = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
const [showPassword, setShowPassword] = React.useState(false);
const disabled = props.value === '' || props.value === undefined || props.disabled;

return (
<div className='relative'>
Expand Down
19 changes: 16 additions & 3 deletions web/src/features/auth/AcceptInvite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Route as AcceptInviteRoute } from '@/routes/accept-invite/index';
import { useMutation } from '@tanstack/react-query';
import { noAuthApi } from '@/common/no-auth-api';
import { toast } from '@/components/ui/use-toast';
import { PasswordInput } from '@/components/ui/password-input';

const formSchema = z
.object({
Expand Down Expand Up @@ -39,7 +40,7 @@ function AcceptInvite() {

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: 'all',
mode: 'onChange',
defaultValues: {
password: '',
confirmPassword: '',
Expand Down Expand Up @@ -99,7 +100,13 @@ function AcceptInvite() {
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type='password' autoCorrect='off' autoCapitalize='none' autoComplete='off' {...field} />
<PasswordInput
autoCorrect='off'
autoCapitalize='off'
autoComplete='off'
spellCheck='false'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand All @@ -113,7 +120,13 @@ function AcceptInvite() {
<FormItem>
<FormLabel>Confirm password</FormLabel>
<FormControl>
<Input type='password' {...field} />
<PasswordInput
autoCorrect='off'
autoCapitalize='off'
autoComplete='off'
spellCheck='false'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand Down
6 changes: 3 additions & 3 deletions web/src/features/auth/ForgotPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface ForgotPasswordRequest {
function ForgotPassword() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: 'all',
mode: 'onChange',
defaultValues: {
email: '',
},
Expand Down Expand Up @@ -72,7 +72,7 @@ function ForgotPassword() {
name='email'
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type='email' {...field} />
</FormControl>
Expand All @@ -83,7 +83,7 @@ function ForgotPassword() {
</CardContent>
<CardFooter>
<Button type='submit' className='w-full'>
Reset password
Request password reset
</Button>
</CardFooter>
</Card>
Expand Down
11 changes: 9 additions & 2 deletions web/src/features/auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { LoginDTO } from '@/common/auth-api';
import { useNavigate } from '@tanstack/react-router';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import Logo from '@/components/layout/Header/Logo';
import { PasswordInput } from '@/components/ui/password-input';

const formSchema = z.object({
email: z
Expand All @@ -27,7 +28,7 @@ function Login() {
const navigate = useNavigate();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: 'all',
mode: 'onChange',
defaultValues: {
email: '',
password: '',
Expand Down Expand Up @@ -77,7 +78,13 @@ function Login() {
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type='password' {...field} />
<PasswordInput
autoCorrect='off'
autoCapitalize='off'
autoComplete='off'
spellCheck='false'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand Down
19 changes: 16 additions & 3 deletions web/src/features/auth/ResetPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { noAuthApi } from '@/common/no-auth-api';
import { toast } from '@/components/ui/use-toast';
import { useNavigate } from '@tanstack/react-router';
import type { FunctionComponent } from '@/common/types';
import { PasswordInput } from '@/components/ui/password-input';

interface ResetPasswordRequest {
password: string;
Expand Down Expand Up @@ -48,7 +49,7 @@ function ResetPassword(): FunctionComponent {

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: 'all',
mode: 'onChange',
defaultValues: {
email: '',
password: '',
Expand Down Expand Up @@ -113,7 +114,13 @@ function ResetPassword(): FunctionComponent {
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type='password' {...field} />
<PasswordInput
autoCorrect='off'
autoCapitalize='off'
autoComplete='off'
spellCheck='false'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand All @@ -127,7 +134,13 @@ function ResetPassword(): FunctionComponent {
<FormItem>
<FormLabel>Confirm your password</FormLabel>
<FormControl>
<Input type='password' autoCorrect='off' autoCapitalize='none' autoComplete='off' {...field} />
<PasswordInput
autoCorrect='off'
autoCapitalize='off'
autoComplete='off'
spellCheck='false'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand Down
Loading