fix: insecure routes not working for SSO (#1587)

This commit is contained in:
Eli Bosley
2025-08-15 12:43:22 -04:00
committed by GitHub
parent 1e0a54d9ef
commit a4ff3c4092
7 changed files with 80 additions and 37 deletions
@@ -37,9 +37,26 @@ jobs:
echo "Source directory does not exist!"
exit 1
fi
# Remove old API docs but preserve other folders
rm -rf docs-repo/docs/API/
mkdir -p docs-repo/docs/API
# Copy all markdown files and maintain directory structure
cp -r source-repo/api/docs/public/. docs-repo/docs/API/
# Clean and copy images directory specifically
rm -rf docs-repo/docs/API/images/
mkdir -p docs-repo/docs/API/images
# Copy images from public/images if they exist
if [ -d "source-repo/api/docs/public/images" ]; then
cp -r source-repo/api/docs/public/images/. docs-repo/docs/API/images/
fi
# Also copy any images from the parent docs/images directory
if [ -d "source-repo/api/docs/images" ]; then
cp -r source-repo/api/docs/images/. docs-repo/docs/API/images/
fi
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
@@ -1512,22 +1512,32 @@ describe('OidcAuthService', () => {
describe('getRedirectUri (private method)', () => {
it('should generate correct redirect URI with localhost (development)', () => {
const getRedirectUri = (service as any).getRedirectUri.bind(service);
const redirectUri = getRedirectUri('localhost:3001');
const redirectUri = getRedirectUri('http://localhost:3000');
expect(redirectUri).toBe('http://localhost:3000/graphql/api/auth/oidc/callback');
});
it('should generate correct redirect URI with non-localhost host', () => {
const getRedirectUri = (service as any).getRedirectUri.bind(service);
// Mock the ConfigService to return a production base URL
configService.get.mockReturnValue('https://example.com');
const redirectUri = getRedirectUri('example.com:443');
const redirectUri = getRedirectUri('https://example.com');
expect(redirectUri).toBe('https://example.com/graphql/api/auth/oidc/callback');
});
it('should handle HTTP protocol for non-localhost hosts', () => {
const getRedirectUri = (service as any).getRedirectUri.bind(service);
const redirectUri = getRedirectUri('http://tower.local');
expect(redirectUri).toBe('http://tower.local/graphql/api/auth/oidc/callback');
});
it('should handle non-standard ports correctly', () => {
const getRedirectUri = (service as any).getRedirectUri.bind(service);
const redirectUri = getRedirectUri('http://example.com:8080');
expect(redirectUri).toBe('http://example.com:8080/graphql/api/auth/oidc/callback');
});
it('should use default redirect URI when no request host provided', () => {
const getRedirectUri = (service as any).getRedirectUri.bind(service);
@@ -36,13 +36,17 @@ export class OidcAuthService {
private readonly validationService: OidcValidationService
) {}
async getAuthorizationUrl(providerId: string, state: string, requestHost?: string): Promise<string> {
async getAuthorizationUrl(
providerId: string,
state: string,
requestOrigin?: string
): Promise<string> {
const provider = await this.oidcConfig.getProvider(providerId);
if (!provider) {
throw new UnauthorizedException(`Provider ${providerId} not found`);
}
const redirectUri = this.getRedirectUri(requestHost);
const redirectUri = this.getRedirectUri(requestOrigin);
// Generate secure state with cryptographic signature
const secureState = this.stateService.generateSecureState(providerId, state);
@@ -110,7 +114,7 @@ export class OidcAuthService {
providerId: string,
code: string,
state: string,
requestHost?: string,
requestOrigin?: string,
fullCallbackUrl?: string
): Promise<string> {
const provider = await this.oidcConfig.getProvider(providerId);
@@ -119,7 +123,7 @@ export class OidcAuthService {
}
try {
const redirectUri = this.getRedirectUri(requestHost);
const redirectUri = this.getRedirectUri(requestOrigin);
// Always use openid-client for consistency
const config = await this.getOrCreateConfig(provider);
@@ -634,30 +638,35 @@ export class OidcAuthService {
return this.validationService.validateProvider(provider);
}
private getRedirectUri(requestHost?: string): string {
// Always use the proxied path through /graphql to match production
if (requestHost && requestHost.includes('localhost')) {
// In development, use the Nuxt proxy at port 3000
return `http://localhost:3000/graphql/api/auth/oidc/callback`;
}
private getRedirectUri(requestOrigin?: string): string {
// If we have the full origin (protocol://host), use it directly
if (requestOrigin) {
// Parse the origin to extract protocol and host
try {
const url = new URL(requestOrigin);
const { protocol, hostname, port } = url;
// In production, use the actual request host or configured base URL
if (requestHost) {
// Parse the host to handle port numbers properly
const isLocalhost = requestHost.includes('localhost');
const protocol = isLocalhost ? 'http' : 'https';
// Reconstruct the URL, removing default ports
let cleanOrigin = `${protocol}//${hostname}`;
// Remove standard ports (:443 for HTTPS, :80 for HTTP)
let cleanHost = requestHost;
if (!isLocalhost) {
if (requestHost.endsWith(':443')) {
cleanHost = requestHost.slice(0, -4); // Remove :443
} else if (requestHost.endsWith(':80')) {
cleanHost = requestHost.slice(0, -3); // Remove :80
// Add port if it's not the default for the protocol
if (
port &&
!(protocol === 'https:' && port === '443') &&
!(protocol === 'http:' && port === '80')
) {
cleanOrigin += `:${port}`;
}
}
return `${protocol}://${cleanHost}/graphql/api/auth/oidc/callback`;
// Special handling for localhost development with Nuxt proxy
if (hostname === 'localhost' && port === '3000') {
return `${cleanOrigin}/graphql/api/auth/oidc/callback`;
}
return `${cleanOrigin}/graphql/api/auth/oidc/callback`;
} catch (e) {
this.logger.warn(`Failed to parse request origin: ${requestOrigin}, error: ${e}`);
}
}
// Fall back to configured BASE_URL or default
+11 -4
View File
@@ -75,10 +75,16 @@ export class RestController {
return res.status(400).send('State parameter is required');
}
// Get the host from the request headers
const host = req.headers.host || undefined;
// Get the host and protocol from the request headers
const protocol = (req.headers['x-forwarded-proto'] as string) || req.protocol || 'http';
const host = (req.headers['x-forwarded-host'] as string) || req.headers.host || undefined;
const requestInfo = host ? `${protocol}://${host}` : undefined;
const authUrl = await this.oidcAuthService.getAuthorizationUrl(providerId, state, host);
const authUrl = await this.oidcAuthService.getAuthorizationUrl(
providerId,
state,
requestInfo
);
this.logger.log(`Redirecting to OIDC provider: ${authUrl}`);
// Manually set redirect headers for better proxy compatibility
@@ -125,6 +131,7 @@ export class RestController {
const host =
(req.headers['x-forwarded-host'] as string) || req.headers.host || 'localhost:3000';
const fullUrl = `${protocol}://${host}${req.url}`;
const requestInfo = `${protocol}://${host}`;
this.logger.debug(`Full callback URL from request: ${fullUrl}`);
@@ -132,7 +139,7 @@ export class RestController {
providerId,
code,
state,
host,
requestInfo,
fullUrl
);
@@ -12,7 +12,7 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
return true;
}
// We may have an SSO token, attempt validation
if (strlen($password) > 800) {
if (strlen($password) > 500) {
if (!preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', $password)) {
my_logger("SSO Login Attempt Failed: Invalid token format");
return false;
@@ -17,7 +17,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php
+ return true;
+ }
+ // We may have an SSO token, attempt validation
+ if (strlen($password) > 800) {
+ if (strlen($password) > 500) {
+ if (!preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', $password)) {
+ my_logger("SSO Login Attempt Failed: Invalid token format");
+ return false;
@@ -24,7 +24,7 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
return true;
}
// We may have an SSO token, attempt validation
if (strlen($password) > 800) {
if (strlen($password) > 500) {
if (!preg_match('/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/', $password)) {
my_logger("SSO Login Attempt Failed: Invalid token format");
return false;