Skip to content

Commit 407ed39

Browse files
[dev] [tofikwest] tofik/microsoft-signin-auth-in-app-portal (#1916)
* feat(auth): add Microsoft sign-in integration and update environment variables * chore: improve error handling for Microsoft sign-in --------- Co-authored-by: Tofik Hasanov <[email protected]>
1 parent 89188e4 commit 407ed39

File tree

13 files changed

+277
-8
lines changed

13 files changed

+277
-8
lines changed

apps/app/.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,8 @@ NEXT_OUTPUT_STANDALONE=false # For deploying on AWS instead of Vercel
5252
FLEET_URL="" # If you want to enable MDM, your hosted url
5353
FLEET_TOKEN="" # If you want to enable MDM
5454

55-
FIRECRAWL_API_KEY="" # To research vendors, Required
55+
FIRECRAWL_API_KEY="" # To research vendors, Required
56+
57+
# Microsoft sign-in
58+
AUTH_MICROSOFT_CLIENT_ID=
59+
AUTH_MICROSOFT_CLIENT_SECRET=

apps/app/src/app/(public)/auth/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default async function Page({
4141

4242
const showGoogle = !!(env.AUTH_GOOGLE_ID && env.AUTH_GOOGLE_SECRET);
4343
const showGithub = !!(env.AUTH_GITHUB_ID && env.AUTH_GITHUB_SECRET);
44+
const showMicrosoft = !!(env.AUTH_MICROSOFT_CLIENT_ID && env.AUTH_MICROSOFT_CLIENT_SECRET);
4445

4546
return (
4647
<div className="flex min-h-dvh flex-col text-foreground">
@@ -56,7 +57,7 @@ export default async function Page({
5657
</CardDescription>
5758
</CardHeader>
5859
<CardContent className="space-y-6 pb-6 px-8">
59-
<LoginForm inviteCode={inviteCode} showGoogle={showGoogle} showGithub={showGithub} />
60+
<LoginForm inviteCode={inviteCode} showGoogle={showGoogle} showGithub={showGithub} showMicrosoft={showMicrosoft} />
6061
</CardContent>
6162
<CardFooter className="pb-10">
6263
<p className="w-full px-6 text-center text-xs text-muted-foreground">

apps/app/src/components/login-form.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { GithubSignIn } from '@/components/github-sign-in';
44
import { GoogleSignIn } from '@/components/google-sign-in';
55
import { MagicLinkSignIn } from '@/components/magic-link';
6+
import { MicrosoftSignIn } from '@/components/microsoft-sign-in';
67
import { Button } from '@comp/ui/button';
78
import { Card, CardContent, CardDescription, CardTitle } from '@comp/ui/card';
89
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@comp/ui/collapsible';
@@ -14,9 +15,10 @@ interface LoginFormProps {
1415
inviteCode?: string;
1516
showGoogle: boolean;
1617
showGithub: boolean;
18+
showMicrosoft: boolean;
1719
}
1820

19-
export function LoginForm({ inviteCode, showGoogle, showGithub }: LoginFormProps) {
21+
export function LoginForm({ inviteCode, showGoogle, showGithub, showMicrosoft }: LoginFormProps) {
2022
const [isOptionsOpen, setIsOptionsOpen] = useState(false);
2123
const [magicLinkState, setMagicLinkState] = useState({ sent: false, email: '' });
2224
const searchParams = useSearchParams();
@@ -70,6 +72,15 @@ export function LoginForm({ inviteCode, showGoogle, showGithub }: LoginFormProps
7072
/>,
7173
);
7274
}
75+
if (showMicrosoft) {
76+
moreOptionsList.push(
77+
<MicrosoftSignIn
78+
key="microsoft"
79+
inviteCode={inviteCode}
80+
searchParams={searchParams as URLSearchParams}
81+
/>,
82+
);
83+
}
7384
if (showGithub) {
7485
moreOptionsList.push(
7586
<GithubSignIn
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use client';
2+
3+
import { authClient } from '@/utils/auth-client';
4+
import { Button } from '@comp/ui/button';
5+
import { Icons } from '@comp/ui/icons';
6+
import { Loader2 } from 'lucide-react';
7+
import { useState } from 'react';
8+
import { toast } from 'sonner';
9+
10+
export function MicrosoftSignIn({
11+
inviteCode,
12+
searchParams,
13+
}: {
14+
inviteCode?: string;
15+
searchParams?: URLSearchParams;
16+
}) {
17+
const [isLoading, setLoading] = useState(false);
18+
19+
const handleSignIn = async () => {
20+
setLoading(true);
21+
22+
try {
23+
// Build the callback URL with search params
24+
const baseURL = window.location.origin;
25+
const path = inviteCode ? `/invite/${inviteCode}` : '/';
26+
const redirectTo = new URL(path, baseURL);
27+
28+
// Append all search params if they exist
29+
if (searchParams) {
30+
searchParams.forEach((value, key) => {
31+
redirectTo.searchParams.append(key, value);
32+
});
33+
}
34+
35+
await authClient.signIn.social({
36+
provider: 'microsoft',
37+
callbackURL: redirectTo.toString(),
38+
});
39+
} catch (error) {
40+
setLoading(false);
41+
42+
console.error('[Microsoft Sign-In] Authentication failed:', {
43+
error,
44+
message: error instanceof Error ? error.message : 'Unknown error',
45+
timestamp: new Date().toISOString(),
46+
});
47+
48+
// Show specific error messages based on error type
49+
if (error instanceof Error) {
50+
if (error.message.includes('redirect_uri_mismatch')) {
51+
toast.error('Configuration error', {
52+
description: 'Redirect URI mismatch. Please contact support.',
53+
});
54+
} else if (error.message.includes('invalid_client')) {
55+
toast.error('Invalid credentials', {
56+
description: 'Microsoft OAuth credentials are invalid. Please contact support.',
57+
});
58+
} else if (error.message.includes('account_not_linked')) {
59+
toast.error('Account linking failed', {
60+
description: 'Unable to link Microsoft account automatically. Please contact support.',
61+
});
62+
console.warn(
63+
'[Microsoft Sign-In] account_not_linked error occurred despite auto-linking being enabled. Check account linking configuration.',
64+
);
65+
} else if (error.message.includes('network') || error.message.includes('fetch')) {
66+
toast.error('Network error', {
67+
description: 'Please check your internet connection and try again.',
68+
});
69+
} else {
70+
toast.error('Sign-in failed', {
71+
description: error.message || 'An unexpected error occurred. Please try again.',
72+
});
73+
}
74+
} else {
75+
toast.error('Failed to sign in with Microsoft', {
76+
description: 'An unexpected error occurred. Please try again.',
77+
});
78+
}
79+
}
80+
};
81+
82+
return (
83+
<Button
84+
onClick={handleSignIn}
85+
className="w-full h-11 font-medium"
86+
variant="outline"
87+
disabled={isLoading}
88+
>
89+
{isLoading ? (
90+
<Loader2 className="h-4 w-4 animate-spin" />
91+
) : (
92+
<>
93+
<Icons.Microsoft className="h-4 w-4" />
94+
Continue with Microsoft
95+
</>
96+
)}
97+
</Button>
98+
);
99+
}

apps/app/src/env.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const env = createEnv({
88
AUTH_GITHUB_ID: z.string().optional(),
99
AUTH_GITHUB_SECRET: z.string().optional(),
1010
AUTH_SECRET: z.string(),
11+
AUTH_MICROSOFT_CLIENT_ID: z.string().optional(),
12+
AUTH_MICROSOFT_CLIENT_SECRET: z.string().optional(),
1113
DATABASE_URL: z.string().min(1),
1214
OPENAI_API_KEY: z.string().optional(),
1315
GROQ_API_KEY: z.string().optional(),
@@ -63,6 +65,8 @@ export const env = createEnv({
6365
AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID,
6466
AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET,
6567
AUTH_SECRET: process.env.AUTH_SECRET,
68+
AUTH_MICROSOFT_CLIENT_ID: process.env.AUTH_MICROSOFT_CLIENT_ID,
69+
AUTH_MICROSOFT_CLIENT_SECRET: process.env.AUTH_MICROSOFT_CLIENT_SECRET,
6670
DATABASE_URL: process.env.DATABASE_URL,
6771
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
6872
GROQ_API_KEY: process.env.GROQ_API_KEY,

apps/app/src/utils/auth.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ if (env.AUTH_GITHUB_ID && env.AUTH_GITHUB_SECRET) {
3939
};
4040
}
4141

42+
if (env.AUTH_MICROSOFT_CLIENT_ID && env.AUTH_MICROSOFT_CLIENT_SECRET) {
43+
socialProviders = {
44+
...socialProviders,
45+
microsoft: {
46+
clientId: env.AUTH_MICROSOFT_CLIENT_ID,
47+
clientSecret: env.AUTH_MICROSOFT_CLIENT_SECRET,
48+
tenantId: 'common', // Allows any Microsoft account
49+
prompt: 'select_account', // Forces account selection
50+
},
51+
};
52+
}
53+
4254
export const auth = betterAuth({
4355
database: prismaAdapter(db, {
4456
provider: 'postgresql',
@@ -204,6 +216,10 @@ export const auth = betterAuth({
204216
},
205217
account: {
206218
modelName: 'Account',
219+
accountLinking: {
220+
enabled: true,
221+
trustedProviders: ['google', 'github', 'microsoft'],
222+
},
207223
},
208224
verification: {
209225
modelName: 'Verification',

apps/portal/.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ NEXT_PUBLIC_BETTER_AUTH_URL="" # http://localhost:30001
1414
APP_AWS_ACCESS_KEY_ID="" # AWS Access Key ID
1515
APP_AWS_SECRET_ACCESS_KEY="" # AWS Secret Access Key
1616
APP_AWS_REGION="" # AWS Region
17-
APP_AWS_BUCKET_NAME="" # AWS Bucket Name
17+
APP_AWS_BUCKET_NAME="" # AWS Bucket Name
18+
19+
# Microsoft sign-in
20+
AUTH_MICROSOFT_CLIENT_ID=
21+
AUTH_MICROSOFT_CLIENT_SECRET=

apps/portal/src/app/(public)/auth/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default async function Page() {
2020
);
2121

2222
const showGoogle = !!(env.AUTH_GOOGLE_ID && env.AUTH_GOOGLE_SECRET);
23+
const showMicrosoft = !!(env.AUTH_MICROSOFT_CLIENT_ID && env.AUTH_MICROSOFT_CLIENT_SECRET);
2324

2425
return (
2526
<div className="flex min-h-dvh flex-col text-foreground">
@@ -36,7 +37,7 @@ export default async function Page() {
3637
</CardHeader>
3738
<CardContent className="space-y-6 pb-6">
3839
{defaultSignInOptions}
39-
<LoginForm showGoogle={showGoogle} />
40+
<LoginForm showGoogle={showGoogle} showMicrosoft={showMicrosoft} />
4041
</CardContent>
4142
<CardFooter className="pb-10">
4243
<div className="from-primary/10 via-primary/5 to-primary/5 rounded-sm bg-gradient-to-r p-4">

apps/portal/src/app/components/login-form.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
'use client';
22

33
import { GoogleSignIn } from './google-sign-in';
4+
import { MicrosoftSignIn } from './microsoft-sign-in';
45
import { useSearchParams } from 'next/navigation';
56

67
interface LoginFormProps {
78
inviteCode?: string;
89
showGoogle: boolean;
10+
showMicrosoft: boolean;
911
}
1012

11-
export function LoginForm({ inviteCode, showGoogle }: LoginFormProps) {
13+
export function LoginForm({ inviteCode, showGoogle, showMicrosoft }: LoginFormProps) {
1214
const searchParams = useSearchParams();
1315

14-
if (!showGoogle) {
16+
if (!showGoogle && !showMicrosoft) {
1517
return;
1618
}
1719

@@ -26,7 +28,8 @@ export function LoginForm({ inviteCode, showGoogle }: LoginFormProps) {
2628
</span>
2729
</div>
2830
<div className="space-y-4 pt-4">
29-
<GoogleSignIn inviteCode={inviteCode} searchParams={searchParams as URLSearchParams} />
31+
{showGoogle && <GoogleSignIn inviteCode={inviteCode} searchParams={searchParams as URLSearchParams} />}
32+
{showMicrosoft && <MicrosoftSignIn inviteCode={inviteCode} searchParams={searchParams as URLSearchParams} />}
3033
</div>
3134
</div>
3235
);
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use client';
2+
3+
import { authClient } from '@/app/lib/auth-client';
4+
import { Button } from '@comp/ui/button';
5+
import { Icons } from '@comp/ui/icons';
6+
import { Loader2 } from 'lucide-react';
7+
import { useState } from 'react';
8+
import { toast } from 'sonner';
9+
10+
export function MicrosoftSignIn({
11+
inviteCode,
12+
searchParams,
13+
}: {
14+
inviteCode?: string;
15+
searchParams?: URLSearchParams;
16+
}) {
17+
const [isLoading, setLoading] = useState(false);
18+
19+
const handleSignIn = async () => {
20+
setLoading(true);
21+
22+
try {
23+
// Build the callback URL with search params
24+
const baseURL = window.location.origin;
25+
const path = inviteCode ? `/invite/${inviteCode}` : '/';
26+
const redirectTo = new URL(path, baseURL);
27+
28+
// Append all search params if they exist
29+
if (searchParams) {
30+
searchParams.forEach((value, key) => {
31+
redirectTo.searchParams.append(key, value);
32+
});
33+
}
34+
35+
await authClient.signIn.social({
36+
provider: 'microsoft',
37+
callbackURL: redirectTo.toString(),
38+
});
39+
} catch (error) {
40+
setLoading(false);
41+
42+
console.error('[Microsoft Sign-In] Authentication failed:', {
43+
error,
44+
message: error instanceof Error ? error.message : 'Unknown error',
45+
timestamp: new Date().toISOString(),
46+
});
47+
48+
// Show specific error messages based on error type
49+
if (error instanceof Error) {
50+
if (error.message.includes('redirect_uri_mismatch')) {
51+
toast.error('Configuration error', {
52+
description: 'Redirect URI mismatch. Please contact support.',
53+
});
54+
} else if (error.message.includes('invalid_client')) {
55+
toast.error('Invalid credentials', {
56+
description: 'Microsoft OAuth credentials are invalid. Please contact support.',
57+
});
58+
} else if (error.message.includes('account_not_linked')) {
59+
toast.error('Account linking failed', {
60+
description: 'Unable to link Microsoft account automatically. Please contact support.',
61+
});
62+
console.warn(
63+
'[Microsoft Sign-In] account_not_linked error occurred despite auto-linking being enabled. Check account linking configuration.',
64+
);
65+
} else if (error.message.includes('network') || error.message.includes('fetch')) {
66+
toast.error('Network error', {
67+
description: 'Please check your internet connection and try again.',
68+
});
69+
} else {
70+
toast.error('Sign-in failed', {
71+
description: error.message || 'An unexpected error occurred. Please try again.',
72+
});
73+
}
74+
} else {
75+
toast.error('Failed to sign in with Microsoft', {
76+
description: 'An unexpected error occurred. Please try again.',
77+
});
78+
}
79+
}
80+
};
81+
82+
return (
83+
<Button
84+
onClick={handleSignIn}
85+
className="w-full h-11 font-medium"
86+
variant="outline"
87+
disabled={isLoading}
88+
>
89+
{isLoading ? (
90+
<Loader2 className="h-4 w-4 animate-spin" />
91+
) : (
92+
<>
93+
<Icons.Microsoft className="h-4 w-4" />
94+
Continue with Microsoft
95+
</>
96+
)}
97+
</Button>
98+
);
99+
}
100+

0 commit comments

Comments
 (0)