-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/all features v2 #20
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
Changes from all commits
7c74753
9181383
b5edb19
858ad53
1b6dac0
4c4305b
49508fb
7bab338
00041a8
2bd303f
1b94d27
a26b21a
b809a14
6fbe787
3bf9162
8312f23
a72c2f7
f3fd65a
262b408
796f242
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,23 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Authentication Routes | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * GitHub OAuth flow for user authentication. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * GitHub OAuth flow and Auth0 SSO/SAML for user authentication. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from 'zod'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { isProduction } from '@/config'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { env, isProduction } from '@/config'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ValidationError } from '@/lib/errors'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { logger } from '@/lib/logger'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isAuth0Configured, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getAuth0AuthorizeUrl, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exchangeAuth0Code, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getAuth0UserInfo, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@/services/auth0'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getDb } from '@/services/db'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getGitHubAuthUrl, authenticateWithGitHub } from '@/services/github'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -212,4 +219,134 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ─── Auth0 SSO Routes ─────────────────────────────────────────────── | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GET /sso - Redirect to Auth0 for SSO login | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.get('/sso', async (request: FastifyRequest, reply: FastifyReply) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isAuth0Configured()) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.status(501).send({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: { code: 'SSO_NOT_CONFIGURED', message: 'SSO is not configured on this server' }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const querySchema = z.object({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connection: z.string().optional(), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const query = querySchema.safeParse(request.query); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const connection = query.success ? query.data.connection : undefined; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const state = crypto.randomUUID(); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reply.setCookie('sso_state', state, { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpOnly: true, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| secure: isProduction, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sameSite: 'lax', | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxAge: 600, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: '/', | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const authUrl = getAuth0AuthorizeUrl(state, connection); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.debug({ connection }, 'Redirecting to Auth0 SSO'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.redirect(authUrl); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GET /sso/callback - Auth0 SSO callback | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.get( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| '/sso/callback', | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config: { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rateLimit: { max: 10, timeWindow: '1 minute' }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async (request: FastifyRequest, reply: FastifyReply) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isAuth0Configured()) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.status(501).send({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: { code: 'SSO_NOT_CONFIGURED', message: 'SSO is not configured' }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const callbackSchema = z.object({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: z.string().min(1), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state: z.string().min(1), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+270
to
+272
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The callback schema requires Useful? React with 👍 / 👎. |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: z.string().optional(), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error_description: z.string().optional(), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = callbackSchema.safeParse(request.query); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!result.success) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new ValidationError('Invalid SSO callback parameters'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { code, state, error, error_description } = result.data; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Validate CSRF state */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const storedState = request.cookies.sso_state; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!state || !storedState || state !== storedState) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warn('SSO state mismatch'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.status(400).send({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: { code: 'INVALID_STATE', message: 'Invalid SSO state parameter' }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reply.clearCookie('sso_state'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warn({ error, error_description }, 'Auth0 SSO error'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.status(400).send({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: { code: 'SSO_ERROR', message: error_description ?? error }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Exchange code for tokens and fetch user info */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tokens = await exchangeAuth0Code(code); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const userInfo = await getAuth0UserInfo(tokens.access_token); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const db = getDb(); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Find existing user by email or create a new one */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let user = await db.user.findFirst({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { email: userInfo.email }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!user) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user = await db.user.create({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| githubId: `auth0_${userInfo.sub}`, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| username: userInfo.nickname ?? userInfo.email.split('@')[0] ?? 'user', | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check failure on line 316 in apps/server/src/routes/auth.ts
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| displayName: userInfo.name || null, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avatarUrl: userInfo.picture || null, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email: userInfo.email, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tier: 'TEAM', | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info({ userId: user.id, email: userInfo.email }, 'New SSO user created'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Generate JWT */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const token = app.jwt.sign( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: user.id, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| username: user.username, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tier: user.tier, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { expiresIn: '7d' } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info({ userId: user.id }, 'SSO authentication successful'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Redirect to web app with token */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const webUrl = new URL('/dashboard', env.WEB_APP_URL); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| webUrl.searchParams.set('token', token); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Putting the signed session token into Useful? React with 👍 / 👎. |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.redirect(webUrl.toString()); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+263
to
+343
Check failureCode scanning / CodeQL Missing rate limiting High
This route handler performs
authorization Error loading related location Loading This route handler performs authorization Error loading related location Loading
Copilot AutofixAI 5 days ago Generally, the fix is to ensure that this authorization route is explicitly rate-limited at the Fastify route level, using the standard Concretely, in app.get(
'/sso/callback',
{
rateLimit: {
max: 10,
timeWindow: '1 minute',
},
},
async (request, reply) => { ... }
);This keeps the same limits (10 requests per minute per IP, assuming default plugin behavior) while using the canonical configuration shape recognized by
Suggested changeset
1
apps/server/src/routes/auth.ts
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GET /sso/status - Check if SSO is available | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.get('/sso/status', async (_request: FastifyRequest, reply: FastifyReply) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.send({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enabled: isAuth0Configured(), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+347
to
+351
Check failureCode scanning / CodeQL Missing rate limiting High
This route handler performs
authorization Error loading related location Loading
Copilot AutofixAI 5 days ago To fix the problem, the Concretely, in
Suggested changeset
1
apps/server/src/routes/auth.ts
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check failure
Code scanning / CodeQL
Missing rate limiting High
Copilot Autofix
AI 5 days ago
To fix the problem, the
/ssoroute should be configured with rate limiting using the same mechanism already used for/sso/callback. Fastify’s rate limit plugin supports per-route configuration via aconfig.rateLimitobject in the route options. The best low-impact fix is therefore to wrap the/ssohandler in a route definition that includesconfig: { rateLimit: { ... } }, mirroring the existing callback configuration but possibly with slightly different limits appropriate for an initiation endpoint.Concretely, in
apps/server/src/routes/auth.ts, edit theapp.get('/sso', ...)declaration (around lines 225–253) to add a second argument: a route options object with aconfig.rateLimitproperty, before the async handler function. For example, configure a modest per-IP rate limit such asmax: 30, timeWindow: '1 minute'. No new imports are needed because Fastify picks up these options via the already-registered rate-limit plugin (as evidenced by the/sso/callbackroute using the same pattern). The rest of the handler body (Auth0 configuration check, cookie setting, redirect) remains unchanged.