diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index bddacf4a0..cdcd5f06c 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -17,7 +17,6 @@ ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access # LOGIN_TOKEN_EXPIRES_IN=15m # REFRESH_TOKEN_EXPIRES_IN=90d # FILE_TOKEN_EXPIRES_IN=1d -# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify # MESSAGING_PROVIDER_GMAIL_ENABLED=false # CALENDAR_PROVIDER_GOOGLE_ENABLED=false # IS_BILLING_ENABLED=false @@ -75,3 +74,5 @@ ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access # PG_SSL_ALLOW_SELF_SIGNED=true # SESSION_STORE_SECRET=replace_me_with_a_random_string_session # ENTERPRISE_KEY=replace_me_with_a_valid_enterprise_key +# SSL_KEY_PATH="./certs/your-cert.key" +# SSL_CERT_PATH="./certs/your-cert.crt" \ No newline at end of file diff --git a/packages/twenty-server/scripts/ssl-generation/README.md b/packages/twenty-server/scripts/ssl-generation/README.md new file mode 100644 index 000000000..379d5df3b --- /dev/null +++ b/packages/twenty-server/scripts/ssl-generation/README.md @@ -0,0 +1,74 @@ +# Local SSL Certificate Generation Script + +This Bash script helps generate self-signed SSL certificates for local development. It uses OpenSSL to create a root certificate authority, a domain certificate, and configures them for local usage. + +## Features +- Generates a private key and root certificate. +- Creates a signed certificate for a specified domain. +- Adds the root certificate to the macOS keychain for trusted usage (macOS only). +- Customizable with default values for easier use. + +## Requirements +- OpenSSL + +## Usage + +### Running the Script + +To generate certificates using the default values: + +```sh +./script.sh +``` + +### Specifying Custom Values + +1. **Domain Name**: Specify the domain name for the certificate. Default is `localhost.com`. +2. **Root Certificate Name**: Specify a name for the root certificate. Default is `myRootCertificate`. +3. **Validity Days**: Specify the number of days the certificate is valid for. Default is `398` days. + +#### Examples: + +1. **Using Default Values**: + ```sh + ./script.sh + ``` + +2. **Custom Domain Name**: + ```sh + ./script.sh example.com + ``` + +3. **Custom Domain Name and Root Certificate Name**: + ```sh + ./script.sh example.com customRootCertificate + ``` + +4. **Custom Domain Name, Root Certificate Name, and Validity Days**: + ```sh + ./script.sh example.com customRootCertificate 398 + ``` + +## Script Details + +1. **Check if OpenSSL is Installed**: Ensures OpenSSL is installed before executing. +2. **Create Directory for Certificates**: Uses `~/certs/{domain}`. +3. **Generate Root Certificate**: Creates a root private key and certificate. +4. **Add Root Certificate to macOS Keychain**: Adds root certificate to macOS trusted store (requires admin privileges). +5. **Generate Domain Key**: Produces a private key for the domain. +6. **Create CSR**: Generates a Certificate Signing Request for the domain. +7. **Generate Signed Certificate**: Signs the domain certificate with the root certificate. + +## Output Files + +The generated files are stored in `~/certs/{domain}`: + +- **Root certificate key**: `{root_cert_name}.key` +- **Root certificate**: `{root_cert_name}.pem` +- **Domain private key**: `{domain}.key` +- **Signed certificate**: `{domain}.crt` + +## Notes + +- If running on non-macOS systems, you'll need to manually add the root certificate to your trusted certificate store. +- Ensure that OpenSSL is installed and available in your PATH. \ No newline at end of file diff --git a/packages/twenty-server/scripts/ssl-generation/script.sh b/packages/twenty-server/scripts/ssl-generation/script.sh new file mode 100755 index 000000000..b8b9f4724 --- /dev/null +++ b/packages/twenty-server/scripts/ssl-generation/script.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Check if OpenSSL is installed +if ! command -v openssl &> /dev/null +then + echo "OpenSSL is not installed. Please install it before running this script." + exit +fi + +# Default values +DOMAIN=${1:-localhost.com} +ROOT_CERT_NAME=${2:-myRootCertificate} +VALIDITY_DAYS=${3:-398} # Default is 825 days + +CERTS_DIR=~/certs/$DOMAIN + +# Create a directory to store the certificates +mkdir -p $CERTS_DIR +cd $CERTS_DIR + +# Generate the private key for the Certificate Authority (CA) +openssl genrsa -aes256 -out ${ROOT_CERT_NAME}.key 2048 + +# Generate the root certificate for the CA +openssl req -x509 -new -nodes -key ${ROOT_CERT_NAME}.key -sha256 -days $VALIDITY_DAYS -out ${ROOT_CERT_NAME}.pem \ + -subj "/C=US/ST=State/L=City/O=MyOrg/OU=MyUnit/CN=MyLocalCA" + +# Add the root certificate to the macOS keychain (requires admin password) +if [[ "$OSTYPE" == "darwin"* ]]; then + sudo security add-trusted-cert -d -r trustRoot -k "/Library/Keychains/System.keychain" ${ROOT_CERT_NAME}.pem +fi + +# Generate the private key for the provided domain +openssl genrsa -out $DOMAIN.key 2048 + +# Create a Certificate Signing Request (CSR) for the provided domain +openssl req -new -key $DOMAIN.key -out $DOMAIN.csr \ + -subj "/C=US/ST=State/L=City/O=MyOrg/OU=MyUnit/CN=*.$DOMAIN" + +# Create a configuration file for certificate extensions +cat > $DOMAIN.ext << EOF +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = $DOMAIN +DNS.2 = *.$DOMAIN +EOF + +# Sign the certificate with the CA +openssl x509 -req -in $DOMAIN.csr -CA ${ROOT_CERT_NAME}.pem -CAkey ${ROOT_CERT_NAME}.key -CAcreateserial \ + -out $DOMAIN.crt -days $VALIDITY_DAYS -sha256 -extfile $DOMAIN.ext + +echo "Certificates generated in the directory $CERTS_DIR:" +echo "- Root certificate: ${ROOT_CERT_NAME}.pem" +echo "- Domain private key: $DOMAIN.key" +echo "- Signed certificate: $DOMAIN.crt" + +# Tips for usage +echo "To use these certificates with a local server, configure your server to use $DOMAIN.crt and $DOMAIN.key." \ No newline at end of file diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts index 0224defaa..56dca7a3a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts @@ -103,8 +103,9 @@ export class SSOAuthController { ); } catch (err) { // TODO: improve error management - res.status(403).send(err.message); - res.redirect(`${this.environmentService.get('FRONT_BASE_URL')}/verify`); + res + .status(403) + .redirect(`${this.environmentService.get('FRONT_BASE_URL')}/verify`); } } diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index e80b41c87..94e7a2a83 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -127,13 +127,12 @@ export class EnvironmentVariables { PG_SSL_ALLOW_SELF_SIGNED = false; // Frontend URL - @IsUrl({ require_tld: false }) + @IsUrl({ require_tld: false, require_protocol: true }) FRONT_BASE_URL: string; - // Server URL - @IsUrl({ require_tld: false }) + @IsUrl({ require_tld: false, require_protocol: true }) @IsOptional() - SERVER_URL: string; + SERVER_URL = 'http://localhost:3000'; @IsString() APP_SECRET: string; @@ -166,10 +165,6 @@ export class EnvironmentVariables { INVITATION_TOKEN_EXPIRES_IN = '30d'; // Auth - @IsUrl({ require_tld: false }) - @IsOptional() - FRONT_AUTH_CALLBACK_URL: string; - @CastToBoolean() @IsOptional() @IsBoolean() @@ -198,11 +193,11 @@ export class EnvironmentVariables { @ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED) AUTH_MICROSOFT_CLIENT_SECRET: string; - @IsUrl({ require_tld: false }) + @IsUrl({ require_tld: false, require_protocol: true }) @ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED) AUTH_MICROSOFT_CALLBACK_URL: string; - @IsUrl({ require_tld: false }) + @IsUrl({ require_tld: false, require_protocol: true }) @ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED) AUTH_MICROSOFT_APIS_CALLBACK_URL: string; @@ -219,7 +214,7 @@ export class EnvironmentVariables { @ValidateIf((env) => env.AUTH_GOOGLE_ENABLED) AUTH_GOOGLE_CLIENT_SECRET: string; - @IsUrl({ require_tld: false }) + @IsUrl({ require_tld: false, require_protocol: true }) @ValidateIf((env) => env.AUTH_GOOGLE_ENABLED) AUTH_GOOGLE_CALLBACK_URL: string; @@ -475,6 +470,15 @@ export class EnvironmentVariables { // milliseconds @CastToPositiveNumber() SERVERLESS_FUNCTION_EXEC_THROTTLE_TTL = 1000; + + // SSL + @IsString() + @ValidateIf((env) => env.SERVER_URL.startsWith('https')) + SSL_KEY_PATH: string; + + @IsString() + @ValidateIf((env) => env.SERVER_URL.startsWith('https')) + SSL_CERT_PATH: string; } export const validate = ( diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index e6d073316..3d0d04954 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -209,7 +209,11 @@ export class SSOService { buildIssuerURL( identityProvider: Pick, ) { - return `${this.environmentService.get('SERVER_URL')}/auth/${identityProvider.type.toLowerCase()}/login/${identityProvider.id}`; + const authorizationUrl = new URL(this.environmentService.get('SERVER_URL')); + + authorizationUrl.pathname = `/auth/${identityProvider.type.toLowerCase()}/login/${identityProvider.id}`; + + return authorizationUrl.toString(); } private isOIDCIdentityProvider( diff --git a/packages/twenty-server/src/main.ts b/packages/twenty-server/src/main.ts index 469845f41..c6b4761c4 100644 --- a/packages/twenty-server/src/main.ts +++ b/packages/twenty-server/src/main.ts @@ -2,6 +2,8 @@ import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; +import fs from 'fs'; + import session from 'express-session'; import bytes from 'bytes'; import { useContainer } from 'class-validator'; @@ -24,6 +26,14 @@ const bootstrap = async () => { bufferLogs: process.env.LOGGER_IS_BUFFER_ENABLED === 'true', rawBody: true, snapshot: process.env.DEBUG_MODE === 'true', + ...(process.env.SSL_KEY_PATH && process.env.SSL_CERT_PATH + ? { + httpsOptions: { + key: fs.readFileSync(process.env.SSL_KEY_PATH), + cert: fs.readFileSync(process.env.SSL_CERT_PATH), + }, + } + : {}), }); const logger = app.get(LoggerService); const environmentService = app.get(EnvironmentService); @@ -68,7 +78,7 @@ const bootstrap = async () => { app.use(session(getSessionStorageOptions(environmentService))); } - await app.listen(process.env.PORT ?? 3000); + await app.listen(environmentService.get('PORT')); }; bootstrap(); diff --git a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx index f84201164..2590bfdcc 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx @@ -76,7 +76,6 @@ yarn command:prod cron:calendar:calendar-event-list-fetch ['AUTH_MICROSOFT_CLIENT_SECRET', '', 'Microsoft client secret'], ['AUTH_MICROSOFT_CALLBACK_URL', 'http://[YourDomain]/auth/microsoft/redirect', 'Microsoft auth callback'], ['AUTH_MICROSOFT_APIS_CALLBACK_URL', 'http://[YourDomain]/auth/microsoft-apis/get-access-token', 'Microsoft APIs auth callback'], - ['FRONT_AUTH_CALLBACK_URL', 'http://localhost:3001/verify ', 'Callback used for Login page'], ['IS_SIGN_UP_DISABLED', 'false', 'Disable sign-up'], ['PASSWORD_RESET_TOKEN_EXPIRES_IN', '5m', 'Password reset token expiration time'], ]}>