Compare commits

...

5 Commits

Author SHA1 Message Date
Eli Bosley
86b4e2cb40 Merge e5b71c6a49 into 1e0a54d9ef 2025-08-15 12:42:39 -04:00
Eli Bosley
e5b71c6a49 fix: adjust SSO password length validation
- Updated the password length validation in the SSO login process from 800 to 500 characters to enhance security and prevent potential issues with excessively long tokens.
- Corresponding changes made in the related snapshot and patch files to ensure consistency across the codebase.
2025-08-15 12:42:35 -04:00
Eli Bosley
0eb7127057 refactor: update OIDC redirect URI handling in service and controller
- Renamed `requestHost` to `requestOrigin` in OidcAuthService methods for clarity.
- Enhanced `getRedirectUri` method to handle full origin URLs, including protocol and non-standard ports.
- Updated tests to reflect changes in redirect URI generation for various scenarios, including localhost and non-localhost hosts.
- Modified RestController to construct request information using protocol and host headers for improved compatibility.
2025-08-15 12:28:10 -04:00
Eli Bosley
75030611fe refactor: improve image handling in Docusaurus PR workflow
- Enhanced the create-docusaurus-pr.yml workflow to cleanly remove old API images and ensure proper copying of images from both the API and parent documentation directories. This update maintains the directory structure and improves the accuracy of the generated documentation.
2025-08-15 12:28:10 -04:00
Eli Bosley
216dc75d66 feat: enhance Docusaurus PR workflow to include image copying
- Updated the create-docusaurus-pr.yml workflow to ensure that images from both the API and parent docs directories are copied to the appropriate location in the generated documentation. This maintains the directory structure and improves the completeness of the documentation.
2025-08-15 12:28:10 -04:00
7 changed files with 80 additions and 37 deletions

View File

@@ -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:

View File

@@ -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);

View File

@@ -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

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
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;