drbh
commited on
Commit
Β·
151f8f5
1
Parent(s):
e30ea26
fix: adjust auth flow
Browse files- app/lib/github-app.server.ts +52 -8
- app/routes/_index.tsx +28 -4
- app/routes/auth.github.callback.tsx +25 -4
- app/routes/debug.tsx +36 -0
- app/routes/webhook.github.tsx +12 -3
app/lib/github-app.server.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
import { App } from "@octokit/app";
|
2 |
import { createAppAuth } from "@octokit/auth-app";
|
3 |
import jwt from "jsonwebtoken";
|
|
|
4 |
|
5 |
// GitHub App configuration - these should be environment variables in production
|
6 |
const GITHUB_APP_ID = process.env.GITHUB_APP_ID;
|
@@ -9,9 +10,21 @@ const GITHUB_APP_CLIENT_ID = process.env.GITHUB_APP_CLIENT_ID;
|
|
9 |
const GITHUB_APP_CLIENT_SECRET = process.env.GITHUB_APP_CLIENT_SECRET;
|
10 |
|
11 |
if (!GITHUB_APP_ID || !GITHUB_APP_PRIVATE_KEY || !GITHUB_APP_CLIENT_ID || !GITHUB_APP_CLIENT_SECRET) {
|
|
|
|
|
|
|
|
|
|
|
12 |
throw new Error('Missing required GitHub App environment variables. Please check your .env file.');
|
13 |
}
|
14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
// For now, we'll hardcode a simple in-memory store
|
16 |
// In production, you'd use a database
|
17 |
const userAuthStore = new Map<string, any>();
|
@@ -58,6 +71,9 @@ export class GitHubAppAuth {
|
|
58 |
throw new Error('GITHUB_CALLBACK_URL environment variable is required');
|
59 |
}
|
60 |
|
|
|
|
|
|
|
61 |
const params = new URLSearchParams({
|
62 |
client_id: GITHUB_APP_CLIENT_ID,
|
63 |
redirect_uri: callbackUrl,
|
@@ -65,7 +81,10 @@ export class GitHubAppAuth {
|
|
65 |
state: state || '',
|
66 |
});
|
67 |
|
68 |
-
|
|
|
|
|
|
|
69 |
}
|
70 |
|
71 |
/**
|
@@ -73,11 +92,15 @@ export class GitHubAppAuth {
|
|
73 |
*/
|
74 |
async handleCallback(code: string, state?: string) {
|
75 |
try {
|
|
|
|
|
|
|
76 |
const { data } = await this.app.oauth.createToken({
|
77 |
code,
|
78 |
});
|
79 |
|
80 |
const { token } = data;
|
|
|
81 |
|
82 |
// Get user information
|
83 |
const userOctokit = await this.app.oauth.getUserOctokit({
|
@@ -85,6 +108,7 @@ export class GitHubAppAuth {
|
|
85 |
});
|
86 |
|
87 |
const { data: user } = await userOctokit.rest.users.getAuthenticated();
|
|
|
88 |
|
89 |
// Store user auth info (in production, save to database)
|
90 |
const userAuth = {
|
@@ -99,10 +123,26 @@ export class GitHubAppAuth {
|
|
99 |
};
|
100 |
|
101 |
userAuthStore.set(user.login, userAuth);
|
|
|
102 |
|
103 |
return userAuth;
|
104 |
-
} catch (error) {
|
105 |
-
console.error('GitHub callback error:'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
throw new Error('Failed to authenticate with GitHub');
|
107 |
}
|
108 |
}
|
@@ -152,12 +192,16 @@ export class GitHubAppAuth {
|
|
152 |
return true;
|
153 |
}
|
154 |
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
|
160 |
-
|
|
|
|
|
|
|
|
|
161 |
}
|
162 |
}
|
163 |
|
|
|
1 |
import { App } from "@octokit/app";
|
2 |
import { createAppAuth } from "@octokit/auth-app";
|
3 |
import jwt from "jsonwebtoken";
|
4 |
+
import { createHmac } from "crypto";
|
5 |
|
6 |
// GitHub App configuration - these should be environment variables in production
|
7 |
const GITHUB_APP_ID = process.env.GITHUB_APP_ID;
|
|
|
10 |
const GITHUB_APP_CLIENT_SECRET = process.env.GITHUB_APP_CLIENT_SECRET;
|
11 |
|
12 |
if (!GITHUB_APP_ID || !GITHUB_APP_PRIVATE_KEY || !GITHUB_APP_CLIENT_ID || !GITHUB_APP_CLIENT_SECRET) {
|
13 |
+
console.error('β Missing required GitHub App environment variables:');
|
14 |
+
console.error('- GITHUB_APP_ID:', GITHUB_APP_ID ? 'β
Set' : 'β Missing');
|
15 |
+
console.error('- GITHUB_APP_PRIVATE_KEY:', GITHUB_APP_PRIVATE_KEY ? 'β
Set' : 'β Missing');
|
16 |
+
console.error('- GITHUB_APP_CLIENT_ID:', GITHUB_APP_CLIENT_ID ? 'β
Set' : 'β Missing');
|
17 |
+
console.error('- GITHUB_APP_CLIENT_SECRET:', GITHUB_APP_CLIENT_SECRET ? 'β
Set' : 'β Missing');
|
18 |
throw new Error('Missing required GitHub App environment variables. Please check your .env file.');
|
19 |
}
|
20 |
|
21 |
+
// Log startup info (safe - no secrets)
|
22 |
+
console.log('π GitHub App Configuration:');
|
23 |
+
console.log('- App ID:', GITHUB_APP_ID);
|
24 |
+
console.log('- Client ID:', GITHUB_APP_CLIENT_ID);
|
25 |
+
console.log('- App Name:', process.env.GITHUB_APP_NAME);
|
26 |
+
console.log('- Callback URL:', process.env.GITHUB_CALLBACK_URL);
|
27 |
+
|
28 |
// For now, we'll hardcode a simple in-memory store
|
29 |
// In production, you'd use a database
|
30 |
const userAuthStore = new Map<string, any>();
|
|
|
71 |
throw new Error('GITHUB_CALLBACK_URL environment variable is required');
|
72 |
}
|
73 |
|
74 |
+
console.log('π Generating OAuth URL with client ID:', GITHUB_APP_CLIENT_ID);
|
75 |
+
console.log('π Callback URL:', callbackUrl);
|
76 |
+
|
77 |
const params = new URLSearchParams({
|
78 |
client_id: GITHUB_APP_CLIENT_ID,
|
79 |
redirect_uri: callbackUrl,
|
|
|
81 |
state: state || '',
|
82 |
});
|
83 |
|
84 |
+
const url = `https://github.com/login/oauth/authorize?${params.toString()}`;
|
85 |
+
console.log('π Generated OAuth URL:', url);
|
86 |
+
|
87 |
+
return url;
|
88 |
}
|
89 |
|
90 |
/**
|
|
|
92 |
*/
|
93 |
async handleCallback(code: string, state?: string) {
|
94 |
try {
|
95 |
+
console.log('π Starting OAuth callback with code:', code.substring(0, 8) + '...');
|
96 |
+
console.log('π Using client ID:', GITHUB_APP_CLIENT_ID);
|
97 |
+
|
98 |
const { data } = await this.app.oauth.createToken({
|
99 |
code,
|
100 |
});
|
101 |
|
102 |
const { token } = data;
|
103 |
+
console.log('β
Successfully obtained OAuth token');
|
104 |
|
105 |
// Get user information
|
106 |
const userOctokit = await this.app.oauth.getUserOctokit({
|
|
|
108 |
});
|
109 |
|
110 |
const { data: user } = await userOctokit.rest.users.getAuthenticated();
|
111 |
+
console.log('β
Successfully obtained user info for:', user.login);
|
112 |
|
113 |
// Store user auth info (in production, save to database)
|
114 |
const userAuth = {
|
|
|
123 |
};
|
124 |
|
125 |
userAuthStore.set(user.login, userAuth);
|
126 |
+
console.log('β
User authentication stored for:', user.login);
|
127 |
|
128 |
return userAuth;
|
129 |
+
} catch (error: any) {
|
130 |
+
console.error('β GitHub callback error details:');
|
131 |
+
console.error('- Error type:', error.constructor.name);
|
132 |
+
console.error('- Error message:', error.message);
|
133 |
+
console.error('- Error status:', error.status);
|
134 |
+
|
135 |
+
if (error.request) {
|
136 |
+
console.error('- Request details:');
|
137 |
+
console.error(' - URL:', error.request.url);
|
138 |
+
console.error(' - Method:', error.request.method);
|
139 |
+
console.error(' - Client ID used:', error.request.client_id);
|
140 |
+
}
|
141 |
+
|
142 |
+
if (error.response?.data) {
|
143 |
+
console.error('- Response data:', error.response.data);
|
144 |
+
}
|
145 |
+
|
146 |
throw new Error('Failed to authenticate with GitHub');
|
147 |
}
|
148 |
}
|
|
|
192 |
return true;
|
193 |
}
|
194 |
|
195 |
+
try {
|
196 |
+
const expectedSignature = `sha256=${createHmac('sha256', webhookSecret)
|
197 |
+
.update(payload, 'utf8')
|
198 |
+
.digest('hex')}`;
|
199 |
|
200 |
+
return signature === expectedSignature;
|
201 |
+
} catch (error) {
|
202 |
+
console.error('Error verifying webhook signature:', error);
|
203 |
+
return false;
|
204 |
+
}
|
205 |
}
|
206 |
}
|
207 |
|
app/routes/_index.tsx
CHANGED
@@ -14,12 +14,14 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|
14 |
const user = await getUserSession(request);
|
15 |
const url = new URL(request.url);
|
16 |
const error = url.searchParams.get("error");
|
|
|
|
|
17 |
|
18 |
-
return json({ user, error });
|
19 |
}
|
20 |
|
21 |
export default function Index() {
|
22 |
-
const { user, error } = useLoaderData<typeof loader>();
|
23 |
const [searchParams] = useSearchParams();
|
24 |
|
25 |
return (
|
@@ -58,9 +60,31 @@ export default function Index() {
|
|
58 |
Authentication Error
|
59 |
</h3>
|
60 |
<div className="mt-2 text-sm text-red-700">
|
61 |
-
{error === "oauth_failed" &&
|
|
|
|
|
|
|
|
|
|
|
62 |
{error === "no_code" && "No authorization code received from GitHub."}
|
63 |
-
{error === "callback_failed" &&
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
</div>
|
65 |
</div>
|
66 |
</div>
|
|
|
14 |
const user = await getUserSession(request);
|
15 |
const url = new URL(request.url);
|
16 |
const error = url.searchParams.get("error");
|
17 |
+
const details = url.searchParams.get("details");
|
18 |
+
const message = url.searchParams.get("message");
|
19 |
|
20 |
+
return json({ user, error, details, message });
|
21 |
}
|
22 |
|
23 |
export default function Index() {
|
24 |
+
const { user, error, details, message } = useLoaderData<typeof loader>();
|
25 |
const [searchParams] = useSearchParams();
|
26 |
|
27 |
return (
|
|
|
60 |
Authentication Error
|
61 |
</h3>
|
62 |
<div className="mt-2 text-sm text-red-700">
|
63 |
+
{error === "oauth_failed" && (
|
64 |
+
<div>
|
65 |
+
<p>OAuth authentication failed.</p>
|
66 |
+
{details && <p className="mt-1 font-mono text-xs">Details: {details}</p>}
|
67 |
+
</div>
|
68 |
+
)}
|
69 |
{error === "no_code" && "No authorization code received from GitHub."}
|
70 |
+
{error === "callback_failed" && (
|
71 |
+
<div>
|
72 |
+
<p>Failed to complete authentication.</p>
|
73 |
+
{message && <p className="mt-1 font-mono text-xs">Error: {message}</p>}
|
74 |
+
</div>
|
75 |
+
)}
|
76 |
+
{error === "expired_code" && (
|
77 |
+
<div>
|
78 |
+
<p>The authorization code has expired or is invalid.</p>
|
79 |
+
<p className="mt-1">Please try signing in again.</p>
|
80 |
+
</div>
|
81 |
+
)}
|
82 |
+
{error === "invalid_client" && (
|
83 |
+
<div>
|
84 |
+
<p>GitHub App configuration error.</p>
|
85 |
+
<p className="mt-1">Please check the client ID and secret.</p>
|
86 |
+
</div>
|
87 |
+
)}
|
88 |
</div>
|
89 |
</div>
|
90 |
</div>
|
app/routes/auth.github.callback.tsx
CHANGED
@@ -8,11 +8,20 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|
8 |
const code = url.searchParams.get("code");
|
9 |
const state = url.searchParams.get("state");
|
10 |
const error = url.searchParams.get("error");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
|
12 |
// Handle OAuth errors
|
13 |
if (error) {
|
14 |
console.error("GitHub OAuth error:", error);
|
15 |
-
return redirect(
|
16 |
}
|
17 |
|
18 |
if (!code) {
|
@@ -34,13 +43,25 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|
34 |
avatar_url: userAuth.avatar_url,
|
35 |
});
|
36 |
|
37 |
-
|
|
|
|
|
|
|
38 |
headers: {
|
39 |
"Set-Cookie": await commitSession(session),
|
40 |
},
|
41 |
});
|
42 |
-
} catch (error) {
|
43 |
console.error("GitHub callback error:", error);
|
44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
}
|
46 |
}
|
|
|
8 |
const code = url.searchParams.get("code");
|
9 |
const state = url.searchParams.get("state");
|
10 |
const error = url.searchParams.get("error");
|
11 |
+
const installation_id = url.searchParams.get("installation_id");
|
12 |
+
const setup_action = url.searchParams.get("setup_action");
|
13 |
+
|
14 |
+
console.log('π OAuth callback received:');
|
15 |
+
console.log('- Code:', code ? code.substring(0, 8) + '...' : 'Missing');
|
16 |
+
console.log('- State:', state);
|
17 |
+
console.log('- Error:', error);
|
18 |
+
console.log('- Installation ID:', installation_id);
|
19 |
+
console.log('- Setup Action:', setup_action);
|
20 |
|
21 |
// Handle OAuth errors
|
22 |
if (error) {
|
23 |
console.error("GitHub OAuth error:", error);
|
24 |
+
return redirect(`/?error=oauth_failed&details=${encodeURIComponent(error)}`);
|
25 |
}
|
26 |
|
27 |
if (!code) {
|
|
|
43 |
avatar_url: userAuth.avatar_url,
|
44 |
});
|
45 |
|
46 |
+
const redirectUrl = installation_id ? "/install" : "/dashboard";
|
47 |
+
console.log('β
OAuth success, redirecting to:', redirectUrl);
|
48 |
+
|
49 |
+
return redirect(redirectUrl, {
|
50 |
headers: {
|
51 |
"Set-Cookie": await commitSession(session),
|
52 |
},
|
53 |
});
|
54 |
+
} catch (error: any) {
|
55 |
console.error("GitHub callback error:", error);
|
56 |
+
|
57 |
+
// Provide more specific error information
|
58 |
+
let errorCode = "callback_failed";
|
59 |
+
if (error.message?.includes("bad_verification_code") || error.message?.includes("incorrect or expired")) {
|
60 |
+
errorCode = "expired_code";
|
61 |
+
} else if (error.message?.includes("client_id")) {
|
62 |
+
errorCode = "invalid_client";
|
63 |
+
}
|
64 |
+
|
65 |
+
return redirect(`/?error=${errorCode}&message=${encodeURIComponent(error.message)}`);
|
66 |
}
|
67 |
}
|
app/routes/debug.tsx
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { json } from "@remix-run/node";
|
2 |
+
import type { LoaderFunctionArgs } from "@remix-run/node";
|
3 |
+
|
4 |
+
export async function loader({ request }: LoaderFunctionArgs) {
|
5 |
+
// Only allow this in development or if a special debug header is set
|
6 |
+
const isDev = process.env.NODE_ENV === "development";
|
7 |
+
const debugHeader = request.headers.get("x-debug-token");
|
8 |
+
const debugToken = process.env.DEBUG_TOKEN;
|
9 |
+
|
10 |
+
if (!isDev && (!debugToken || debugHeader !== debugToken)) {
|
11 |
+
throw new Response("Not Found", { status: 404 });
|
12 |
+
}
|
13 |
+
|
14 |
+
// Safe environment info (no secrets)
|
15 |
+
const envInfo = {
|
16 |
+
NODE_ENV: process.env.NODE_ENV,
|
17 |
+
GITHUB_APP_ID: process.env.GITHUB_APP_ID ? 'β
Set' : 'β Missing',
|
18 |
+
GITHUB_APP_NAME: process.env.GITHUB_APP_NAME ? 'β
Set' : 'β Missing',
|
19 |
+
GITHUB_APP_PRIVATE_KEY: process.env.GITHUB_APP_PRIVATE_KEY ? 'β
Set' : 'β Missing',
|
20 |
+
GITHUB_APP_CLIENT_ID: process.env.GITHUB_APP_CLIENT_ID ? `β
Set (${process.env.GITHUB_APP_CLIENT_ID?.substring(0, 8)}...)` : 'β Missing',
|
21 |
+
GITHUB_APP_CLIENT_SECRET: process.env.GITHUB_APP_CLIENT_SECRET ? 'β
Set' : 'β Missing',
|
22 |
+
GITHUB_WEBHOOK_SECRET: process.env.GITHUB_WEBHOOK_SECRET ? 'β
Set' : 'β Missing',
|
23 |
+
GITHUB_CALLBACK_URL: process.env.GITHUB_CALLBACK_URL || 'β Missing',
|
24 |
+
SESSION_SECRET: process.env.SESSION_SECRET ? 'β
Set' : 'β Missing',
|
25 |
+
};
|
26 |
+
|
27 |
+
return json({
|
28 |
+
timestamp: new Date().toISOString(),
|
29 |
+
environment: envInfo,
|
30 |
+
request: {
|
31 |
+
url: request.url,
|
32 |
+
method: request.method,
|
33 |
+
headers: Object.fromEntries(request.headers.entries()),
|
34 |
+
}
|
35 |
+
});
|
36 |
+
}
|
app/routes/webhook.github.tsx
CHANGED
@@ -7,21 +7,30 @@ export async function action({ request }: ActionFunctionArgs) {
|
|
7 |
return json({ error: "Method not allowed" }, { status: 405 });
|
8 |
}
|
9 |
|
|
|
|
|
|
|
10 |
try {
|
11 |
-
|
12 |
const signature = request.headers.get("x-hub-signature-256") || "";
|
13 |
const event = request.headers.get("x-github-event") || "";
|
14 |
const delivery = request.headers.get("x-github-delivery") || "";
|
15 |
|
|
|
|
|
16 |
// Verify webhook signature
|
17 |
if (!githubApp.verifyWebhookSignature(payload, signature)) {
|
18 |
console.error("Invalid webhook signature");
|
19 |
return json({ error: "Invalid signature" }, { status: 401 });
|
20 |
}
|
21 |
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
-
console.log(`π₯ Received GitHub webhook: ${event} (${delivery})`);
|
25 |
console.log("Event data:", JSON.stringify(eventData, null, 2));
|
26 |
|
27 |
// Handle different webhook events
|
|
|
7 |
return json({ error: "Method not allowed" }, { status: 405 });
|
8 |
}
|
9 |
|
10 |
+
let payload: string;
|
11 |
+
let eventData: any;
|
12 |
+
|
13 |
try {
|
14 |
+
payload = await request.text();
|
15 |
const signature = request.headers.get("x-hub-signature-256") || "";
|
16 |
const event = request.headers.get("x-github-event") || "";
|
17 |
const delivery = request.headers.get("x-github-delivery") || "";
|
18 |
|
19 |
+
console.log(`π₯ Received GitHub webhook: ${event} (${delivery})`);
|
20 |
+
|
21 |
// Verify webhook signature
|
22 |
if (!githubApp.verifyWebhookSignature(payload, signature)) {
|
23 |
console.error("Invalid webhook signature");
|
24 |
return json({ error: "Invalid signature" }, { status: 401 });
|
25 |
}
|
26 |
|
27 |
+
try {
|
28 |
+
eventData = JSON.parse(payload);
|
29 |
+
} catch (parseError) {
|
30 |
+
console.error("Failed to parse webhook payload:", parseError);
|
31 |
+
return json({ error: "Invalid JSON payload" }, { status: 400 });
|
32 |
+
}
|
33 |
|
|
|
34 |
console.log("Event data:", JSON.stringify(eventData, null, 2));
|
35 |
|
36 |
// Handle different webhook events
|