Skip to content
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

chore: New Auth for the Projects #3978

Merged
merged 2 commits into from
Aug 21, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ _build
# misc
.DS_Store
*.pem
!https/*.pem
/.idea

# logs
Expand Down
81 changes: 81 additions & 0 deletions apps/builder/app/shared/router-utils/origins.server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect, test } from "@jest/globals";
import { parseBuilderUrl } from "./origins.server";

test("parseBuilderUrl wstd.dev", async () => {
expect(
parseBuilderUrl("https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb.wstd.dev")
).toMatchInlineSnapshot(`
{
"projectId": "090e6e14-ae50-4b2e-bd22-71733cec05bb",
"sourceOrigin": "https://wstd.dev",
}
`);
});

test("parseBuilderUrl localhost", async () => {
expect(
parseBuilderUrl("https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb.localhost")
).toMatchInlineSnapshot(`
{
"projectId": "090e6e14-ae50-4b2e-bd22-71733cec05bb",
"sourceOrigin": "https://localhost",
}
`);
});

test("parseBuilderUrl localhost", async () => {
expect(parseBuilderUrl("https://p-eee.localhost")).toMatchInlineSnapshot(`
{
"projectId": undefined,
"sourceOrigin": "https://p-eee.localhost",
}
`);
});

test("parseBuilderUrl development.webstudio.is", async () => {
expect(
parseBuilderUrl(
"https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb.development.webstudio.is"
)
).toMatchInlineSnapshot(`
{
"projectId": "090e6e14-ae50-4b2e-bd22-71733cec05bb",
"sourceOrigin": "https://development.webstudio.is",
}
`);
});

test("parseBuilderUrl development.webstudio.is", async () => {
expect(
parseBuilderUrl(
"https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb-dot-main.development.webstudio.is"
)
).toMatchInlineSnapshot(`
{
"projectId": "090e6e14-ae50-4b2e-bd22-71733cec05bb",
"sourceOrigin": "https://development.webstudio.is",
}
`);
});

test("parseBuilderUrl apps.webstudio.is", async () => {
expect(
parseBuilderUrl(
"https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb.apps.webstudio.is"
)
).toMatchInlineSnapshot(`
{
"projectId": "090e6e14-ae50-4b2e-bd22-71733cec05bb",
"sourceOrigin": "https://apps.webstudio.is",
}
`);
});

test("parseBuilderUrl apps.webstudio.is", async () => {
expect(parseBuilderUrl("https://apps.webstudio.is")).toMatchInlineSnapshot(`
{
"projectId": undefined,
"sourceOrigin": "https://apps.webstudio.is",
}
`);
});
81 changes: 81 additions & 0 deletions apps/builder/app/shared/router-utils/origins.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
export const getRequestOrigin = (request: Request) => {
const url = new URL(request.url);

// Vercel overwrites x-forwarded-host at the edge level, even if our header is set.
// We use custom header x-forwarded-ws-host to get the original host as a workaround.
url.host =
request.headers.get("x-forwarded-ws-host") ??
request.headers.get("x-forwarded-host") ??
url.host;
url.protocol = request.headers.get("x-forwarded-proto") ?? "https";
return url.origin;
};

export const isCanvas = (urlStr: string): boolean => {
const url = new URL(urlStr);
const projectId = url.searchParams.get("projectId");

return projectId !== null;
};

// For easier detecting the builder URL
const buildProjectDomainPrefix = "p-";

export const parseBuilderUrl = (urlStr: string) => {
const url = new URL(urlStr);

const fragments = url.host.split(".");
// Regular expression to match the prefix, UUID, and any optional string after '-dot-'
const re =
/^(?<prefix>[a-z-]+)(?<uuid>[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})(-dot-(?<branch>.*))?/;
const match = fragments[0].match(re);

// Extract prefix, projectId (UUID), and branch (if exists)
const prefix = match?.groups?.prefix;
const projectId = match?.groups?.uuid;

if (prefix !== buildProjectDomainPrefix) {
return {
projectId: undefined,
sourceOrigin: url.origin,
};
}

if (projectId === undefined) {
return {
projectId: undefined,
sourceOrigin: url.origin,
};
}

fragments[0] = fragments[0].replace(re, "");

const sourceUrl = new URL(url.origin);
sourceUrl.protocol = "https";
sourceUrl.host = fragments.filter(Boolean).join(".");

return {
projectId,
sourceOrigin: sourceUrl.origin,
};
};

export const isBuilderUrl = (urlStr: string): boolean => {
const { projectId } = parseBuilderUrl(urlStr);
return projectId !== undefined;
};

export function getAuthorizationServerOrigin(request: Request): string;
export function getAuthorizationServerOrigin(origin: string): string;

// eslint-disable-next-line func-style
export function getAuthorizationServerOrigin(
request: string | Request
istarkov marked this conversation as resolved.
Show resolved Hide resolved
): string {
const origin =
typeof request === "string"
? new URL(request).origin
: getRequestOrigin(request);
const { sourceOrigin } = parseBuilderUrl(origin);
return sourceOrigin;
}
1 change: 0 additions & 1 deletion apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"@vercel/remix": "2.11.0",
"@vitejs/plugin-basic-ssl": "^1.1.0",
"@webstudio-is/ai": "workspace:*",
"@webstudio-is/asset-uploader": "workspace:*",
"@webstudio-is/authorization-token": "workspace:*",
Expand Down
116 changes: 79 additions & 37 deletions apps/builder/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,85 @@
import { resolve } from "node:path";
import { defineConfig, type Plugin } from "vite";
import { defineConfig, type CorsOptions } from "vite";
import { vitePlugin as remix } from "@remix-run/dev";
import { vercelPreset } from "@vercel/remix/vite";
import basicSsl from "@vitejs/plugin-basic-ssl";
import type { IncomingMessage } from "node:http";
import {
getAuthorizationServerOrigin,
isBuilderUrl,
} from "./app/shared/router-utils/origins.server";
import { readFileSync } from "node:fs";

export default defineConfig(({ mode }) => ({
plugins: [
basicSsl({
name: "dev",
domains: ["wstd.dev", "*.wstd.dev"],
certDir: ".dev/cert",
}) as Plugin<unknown>,
remix({
presets: [vercelPreset()],
}),
],
resolve: {
conditions: ["webstudio", "import", "module", "browser", "default"],
alias: [
{
find: "~",
replacement: resolve("app"),
},
export default defineConfig(({ mode }) => {
if (mode === "development") {
// Enable self-signed certificates for development service 2 service fetch calls.
// This is particularly important for secure communication with the oauth.ws.token endpoint.
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
}

return {
plugins: [
remix({
presets: [vercelPreset()],
}),
],
},
define: {
"process.env.NODE_ENV": JSON.stringify(mode),
},
ssr: {
external: ["@webstudio-is/prisma-client"],
},
server: {
host: true,
// Needed for SSL
proxy: {},
https: {
cert: ".dev/cert.pem",
key: ".dev/key.pem",
resolve: {
conditions: ["webstudio", "import", "module", "browser", "default"],
alias: [
{
find: "~",
replacement: resolve("app"),
},
],
},
define: {
"process.env.NODE_ENV": JSON.stringify(mode),
},
ssr: {
external: ["@webstudio-is/prisma-client"],
},
server: {
// Service-to-service OAuth token call requires a specified host for the wstd.dev domain
host: "wstd.dev",
// Needed for SSL
proxy: {},

https: {
key: readFileSync("../../https/privkey.pem"),
cert: readFileSync("../../https/fullchain.pem"),
},

cors: ((
req: IncomingMessage,
callback: (error: Error | null, options: CorsOptions | null) => void
) => {
// Handle CORS preflight requests in development to mimic Remix production behavior
if (req.method === "OPTIONS") {
if (req.headers.origin != null && req.url != null) {
const url = new URL(req.url, `https://${req.headers.host}`);

// Allow CORS for /logout path when requested from the authorization server
if (url.pathname === "/logout" && isBuilderUrl(url.href)) {
return callback(null, {
origin: getAuthorizationServerOrigin(url.href),
preflightContinue: false,
credentials: true,
});
}
}

// Respond with method not allowed for other preflight requests
return callback(null, {
preflightContinue: false,
optionsSuccessStatus: 405,
});
}

// Disable CORS for all other requests
return callback(null, {
origin: false,
});
}) as never,
},
},
envPrefix: "GITHUB_",
}));
envPrefix: "GITHUB_",
};
});
33 changes: 33 additions & 0 deletions https/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Admin only

Based on this article https://dev.to/istarkov/fast-and-easy-way-to-setup-web-developer-certificates-450e

```bash
rm -rf /tmp/certbot/
rm -rf /tmp/letsencrypt/

mkdir -p /tmp/certbot/
mkdir -p /tmp/letsencrypt/

CLOUDFLARE_API_KEY=$(infisical secrets get WSTD_DEV-CLOUDFLARE_ZONE_TOKEN --path='/CLI' --env=staging --plain)

cat > /tmp/certbot/cloudflare.ini <<-DOCKERFILE
dns_cloudflare_api_token = ${CLOUDFLARE_API_KEY}
DOCKERFILE

docker run -it --rm --name certbot \
-v "/tmp/letsencrypt/data:/etc/letsencrypt" \
-v "/tmp/certbot:/local/certbot" \
certbot/dns-cloudflare certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /local/certbot/cloudflare.ini \
--agree-tos \
--noninteractive \
-m [email protected] \
-d wstd.dev \
-d '*.wstd.dev'


cp /tmp/letsencrypt/data/live/wstd.dev/fullchain.pem ./https/fullchain.pem
cp /tmp/letsencrypt/data/live/wstd.dev/privkey.pem ./https/privkey.pem
```
47 changes: 47 additions & 0 deletions https/fullchain.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
-----BEGIN CERTIFICATE-----
MIIDfjCCAwSgAwIBAgISA1wtdJI3Uetly1bjoFFpx7mvMAoGCCqGSM49BAMDMDIx
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
NTAeFw0yNDA4MjExNTU0MDBaFw0yNDExMTkxNTUzNTlaMBMxETAPBgNVBAMTCHdz
dGQuZGV2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYBhNm2XuyZVtrAMztcPn
7KygzwmIJhiebPrqGNof/YxxbnaCwo5GX1zJJMc7wqrjwh9x4bIXxE9YzJtFKNCW
ZKOCAhcwggITMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUftjOanJD7tOZ6L0dWn9s
26rtVC8wHwYDVR0jBBgwFoAUnytfzzwhT50Et+0rLMTGcIvS1w0wVQYIKwYBBQUH
AQEESTBHMCEGCCsGAQUFBzABhhVodHRwOi8vZTUuby5sZW5jci5vcmcwIgYIKwYB
BQUHMAKGFmh0dHA6Ly9lNS5pLmxlbmNyLm9yZy8wHwYDVR0RBBgwFoIKKi53c3Rk
LmRldoIId3N0ZC5kZXYwEwYDVR0gBAwwCjAIBgZngQwBAgEwggEFBgorBgEEAdZ5
AgQCBIH2BIHzAPEAdgAZmBBxCfDWUi4wgNKeP2S7g24ozPkPUo7u385KPxa0ygAA
AZF12qsVAAAEAwBHMEUCIQDC31THRAyaXWHbhyREtQ9/C0K1mhe+1igmXe/i5U9Z
MAIgbc914ew2ouLErvkGJwGK4UukzeVN4gUH/JmV7Md87isAdwB2/4g/Crb7lVHC
Ycz1h7o0tKTNuyncaEIKn+ZnTFo6dAAAAZF12qtWAAAEAwBIMEYCIQCBR3lxwjPH
bL3ohHp9hNRI4rSWjYjFg3w3E1ISuugGcgIhANgEFRuLtz8b+3mcK9o37MdJf0QQ
wm8G8zCjCTcRMypbMAoGCCqGSM49BAMDA2gAMGUCMQC57jYQQOzuMA3zsCno+HxH
pJqXbkSO6jAcjKFYeFLqAat1Zdu+TXrDib7iixfdbOQCMB/dvi+J9RPsh0194ebu
UFpBDtOKliPQ+V/Dhuc669FS+m0Tw66vMmRA32/nXLDm+g==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEVzCCAj+gAwIBAgIRAIOPbGPOsTmMYgZigxXJ/d4wDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDELMAkGA1UEAxMCRTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNCzqK
a2GOtu/cX1jnxkJFVKtj9mZhSAouWXW0gQI3ULc/FnncmOyhKJdyIBwsz9V8UiBO
VHhbhBRrwJCuhezAUUE8Wod/Bk3U/mDR+mwt4X2VEIiiCFQPmRpM5uoKrNijgfgw
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSfK1/PPCFPnQS37SssxMZw
i9LXDTAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAH3KdNEVCQdqk0LKyuNImTKdRJY1C
2uw2SJajuhqkyGPY8C+zzsufZ+mgnhnq1A2KVQOSykOEnUbx1cy637rBAihx97r+
bcwbZM6sTDIaEriR/PLk6LKs9Be0uoVxgOKDcpG9svD33J+G9Lcfv1K9luDmSTgG
6XNFIN5vfI5gs/lMPyojEMdIzK9blcl2/1vKxO8WGCcjvsQ1nJ/Pwt8LQZBfOFyV
XP8ubAp/au3dc4EKWG9MO5zcx1qT9+NXRGdVWxGvmBFRAajciMfXME1ZuGmk3/GO
koAM7ZkjZmleyokP1LGzmfJcUd9s7eeu1/9/eg5XlXd/55GtYjAM+C4DG5i7eaNq
cm2F+yxYIPt6cbbtYVNJCGfHWqHEQ4FYStUyFnv8sjyqU8ypgZaNJ9aVcWSICLOI
E1/Qv/7oKsnZCWJ926wU6RqG1OYPGOi1zuABhLw61cuPVDT28nQS/e6z95cJXq0e
K1BcaJ6fJZsmbjRgD5p3mvEf5vdQM7MCEvU0tHbsx2I5mHHJoABHb8KVBgWp/lcX
GWiWaeOyB7RP+OfDtvi2OsapxXiV7vNVs7fMlrRjY1joKaqmmycnBvAq14AEbtyL
sVfOS66B8apkeFX2NY4XPEYV4ZSCe8VHPrdrERk2wILG3T/EGmSIkCYVUMSnjmJd
VQD9F6Na/+zmXCc=
-----END CERTIFICATE-----
5 changes: 5 additions & 0 deletions https/privkey.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgBeMEctMFlA2db7fL
JdPevFl/WxG+lfD+FlmhbFEI4IChRANCAARgGE2bZe7JlW2sAzO1w+fsrKDPCYgm
GJ5s+uoY2h/9jHFudoLCjkZfXMkkxzvCquPCH3HhshfET1jMm0Uo0JZk
-----END PRIVATE KEY-----
Loading
Loading