Skip to content
Open
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
116 changes: 116 additions & 0 deletions wavefront/client/src/config/authenticators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,122 @@ export const AUTHENTICATOR_PROVIDERS_CONFIG: Record<AuthenticatorType, ProviderC
},
},
},
microsoft_adfs: {
name: 'Microsoft ADFS (OIDC)',
badge: {
bg: 'bg-purple-100',
text: 'text-purple-800',
},
parameters: {
client_id: {
type: 'string',
default: '',
required: true,
description: 'OIDC client ID set when creating the ADFS Application Group',
placeholder: 'wavefront-adfs',
},
client_secret: {
type: 'string',
default: '',
required: true,
description: 'OIDC client secret generated by ADFS',
placeholder: 'long-opaque-secret-value',
},
authority: {
type: 'string',
default: '',
required: true,
description: 'ADFS server base URL (scheme + host, no path)',
placeholder: 'https://fs.yourdomain.com',
pattern: '^https://.+',
},
redirect_uri: {
type: 'string',
default: '',
required: true,
description: 'OIDC callback URL registered as a Redirect URI in ADFS',
placeholder: 'https://yourapp.com/v1/oauth/microsoft-adfs/callback',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Correct the redirect_uri placeholder path.

The placeholder shows /v1/oauth/microsoft-adfs/callback, but the server-side callback route is registered at /v1/oauth/adfs/callback (without "microsoft-" prefix). Users copying this placeholder will configure an incorrect redirect_uri, causing authentication callbacks to fail.

📝 Suggested fix
-        placeholder: 'https://yourapp.com/v1/oauth/microsoft-adfs/callback',
+        placeholder: 'https://yourapp.com/v1/oauth/adfs/callback',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
placeholder: 'https://yourapp.com/v1/oauth/microsoft-adfs/callback',
placeholder: 'https://yourapp.com/v1/oauth/adfs/callback',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wavefront/client/src/config/authenticators.ts` at line 225, Update the
redirect_uri placeholder string used in the ADFS OAuth configuration: locate the
object/constant in authenticators.ts where the property placeholder is set to
'https://yourapp.com/v1/oauth/microsoft-adfs/callback' and change it to
'https://yourapp.com/v1/oauth/adfs/callback' so it matches the server-side
callback route; ensure the placeholder value is the only change and keep
surrounding object keys (e.g., placeholder) intact.

pattern: '^https?://.+',
},
client_redirect_success_url: {
type: 'string',
default: '',
required: true,
description: 'URL to redirect users after successful authentication',
placeholder: 'https://yourapp.com/dashboard',
pattern: '^https?://.+',
},
client_redirect_failure_url: {
type: 'string',
default: '',
required: true,
description: 'URL to redirect users after failed authentication',
placeholder: 'https://yourapp.com/login?error=auth_failed',
pattern: '^https?://.+',
},
scopes: {
type: 'array',
default: ['openid', 'profile', 'email'],
required: true,
description: 'OIDC scopes to request',
placeholder: 'openid, profile, email',
},
response_type: {
type: 'string',
default: 'code',
required: false,
description: 'OAuth response type',
},
response_mode: {
type: 'string',
default: 'query',
required: false,
description: 'OAuth response mode',
},
authorize_path: {
type: 'string',
default: '/adfs/oauth2/authorize',
required: false,
description: 'Authorize endpoint path under the authority (override only if non-standard)',
placeholder: '/adfs/oauth2/authorize',
},
token_path: {
type: 'string',
default: '/adfs/oauth2/token',
required: false,
description: 'Token endpoint path under the authority (override only if non-standard)',
placeholder: '/adfs/oauth2/token',
},
jwks_path: {
type: 'string',
default: '/adfs/discovery/keys',
required: false,
description: 'JWKS endpoint path under the authority (override only if non-standard)',
placeholder: '/adfs/discovery/keys',
},
expected_issuer: {
type: 'string',
default: '',
required: false,
description: 'Expected `iss` claim value from the OIDC discovery doc. Leave empty to skip issuer check.',
placeholder: 'https://fs.yourdomain.com/adfs',
},
clock_skew_seconds: {
type: 'number',
default: 60,
min: 0,
max: 600,
required: false,
description: 'Allowed clock skew (seconds) when validating exp/nbf claims',
},
verify_ssl: {
type: 'boolean',
default: true,
required: false,
description: 'Verify the ADFS server TLS certificate. Disable ONLY for local testing against self-signed IdPs.',
},
},
},
email_password: {
name: 'Email & Password',
badge: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { z } from 'zod';

const createAuthenticatorSchema = z.object({
authName: z.string().min(1, 'Authenticator name is required'),
authType: z.enum(['google_oauth', 'microsoft_oauth', 'email_password']),
authType: z.enum(['google_oauth', 'microsoft_oauth', 'microsoft_adfs', 'email_password']),
authDesc: z.string().optional(),
});

Expand Down Expand Up @@ -383,7 +383,7 @@ const CreateAuthenticatorDialog: React.FC<CreateAuthenticatorDialogProps> = ({

return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[90vh] max-w-4xl overflow-y-auto">
<DialogContent className="max-h-[90vh] w-[95vw] !max-w-6xl overflow-y-auto sm:!max-w-6xl">
<DialogHeader>
<DialogTitle>Create New Authenticator</DialogTitle>
<DialogDescription>Configure a new authentication provider for {selectedApp?.app_name}</DialogDescription>
Expand Down
13 changes: 7 additions & 6 deletions wavefront/client/src/pages/apps/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DatasourcesIcon,
ModelInferenceIcon,
ModelRepositoryIcon,
PermissionIcon,
PhoneIcon,
RagIcon,
WorkflowIcon,
Expand All @@ -21,12 +22,12 @@ const navItems = [
link: `/apps/:appId/agents`,
description: 'Manage and configure agents for this application',
},
// {
// name: 'Authenticators',
// icon: PermissionIcon,
// link: `/apps/:appId/authenticators`,
// description: 'Manage authentication provider configurations',
// },
{
name: 'Authenticators',
icon: PermissionIcon,
link: `/apps/:appId/authenticators`,
description: 'Manage authentication provider configurations',
},
{
id: 'datasources',
name: 'Datasources',
Expand Down
21 changes: 20 additions & 1 deletion wavefront/client/src/types/authenticator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IApiResponse } from '@app/lib/axios';

// Authenticator type union matching API auth_type field
export type AuthenticatorType = 'google_oauth' | 'microsoft_oauth' | 'email_password';
export type AuthenticatorType = 'google_oauth' | 'microsoft_oauth' | 'microsoft_adfs' | 'email_password';

// Main Authenticator entity interface
export interface Authenticator {
Expand Down Expand Up @@ -76,6 +76,25 @@ export interface MicrosoftOAuthConfig {
response_mode?: string;
}

// Microsoft ADFS (OIDC) specific config interface
export interface MicrosoftADFSConfig {
client_id: string;
client_secret: string;
authority: string;
redirect_uri: string;
client_redirect_success_url: string;
client_redirect_failure_url: string;
scopes: string[];
response_type?: string;
response_mode?: string;
authorize_path?: string;
token_path?: string;
jwks_path?: string;
expected_issuer?: string;
clock_skew_seconds?: number;
verify_ssl?: boolean;
}

// Email/Password specific config interface
export interface EmailPasswordConfig {
password_policy: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""add username to user table

Revision ID: a1b2c3d4e5f8
Revises: 3b5b1bf90e6c
Create Date: 2026-06-11 20:08:00.000000

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'a1b2c3d4e5f8'
down_revision: Union[str, None] = '3b5b1bf90e6c'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
'user',
sa.Column('username', sa.String(length=150), nullable=True),
)
op.create_unique_constraint('uq_user_username', 'user', ['username'])


def downgrade() -> None:
op.drop_constraint('uq_user_username', 'user', type_='unique')
op.drop_column('user', 'username')
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class User(Base):
primary_key=True, default=uuid.uuid4, index=True
)
email: Mapped[str] = mapped_column(nullable=False, unique=True)
username: Mapped[Optional[str]] = mapped_column(nullable=True, unique=True)
password: Mapped[str] = mapped_column(nullable=False)
first_name: Mapped[str] = mapped_column(nullable=False)
last_name: Mapped[str] = mapped_column(nullable=False)
Expand Down Expand Up @@ -46,6 +47,7 @@ def to_dict(self):
return {
'id': str(self.id),
'email': self.email,
'username': self.username,
'first_name': self.first_name,
'last_name': self.last_name,
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ def validate_google_oauth_config(config: Dict[str, Any]) -> List[str]:
"""Validate Google OAuth configuration and return list of errors."""
errors = []

required_fields = ['client_id', 'client_secret', 'redirect_uri']
required_fields = [
'client_id',
'client_secret',
'redirect_uri',
'client_redirect_success_url',
'client_redirect_failure_url',
]
for field in required_fields:
if not config.get(field):
errors.append(f'Missing required field: {field}')
Expand All @@ -24,6 +30,11 @@ def validate_google_oauth_config(config: Dict[str, Any]) -> List[str]:
):
errors.append('redirect_uri must be a valid HTTP/HTTPS URL')

for field in ('client_redirect_success_url', 'client_redirect_failure_url'):
value = config.get(field)
if value and not (value.startswith('http://') or value.startswith('https://')):
errors.append(f'{field} must be a valid HTTP/HTTPS URL')

# Validate scopes
scopes = config.get('scopes', [])
if not isinstance(scopes, list) or len(scopes) == 0:
Expand Down Expand Up @@ -61,6 +72,32 @@ def validate_microsoft_oauth_config(config: Dict[str, Any]) -> List[str]:
return errors


def validate_microsoft_adfs_config(config: Dict[str, Any]) -> List[str]:
"""Validate Microsoft ADFS configuration and return list of errors."""
errors = []

required_fields = ['client_id', 'client_secret', 'authority', 'redirect_uri']
for field in required_fields:
if not config.get(field):
errors.append(f'Missing required field: {field}')

authority = config.get('authority', '')
if authority and not authority.startswith('https://'):
errors.append('authority must be a valid HTTPS URL')

redirect_uri = config.get('redirect_uri')
if redirect_uri and not (
redirect_uri.startswith('http://') or redirect_uri.startswith('https://')
):
errors.append('redirect_uri must be a valid HTTP/HTTPS URL')

scopes = config.get('scopes', [])
if not isinstance(scopes, list) or len(scopes) == 0:
errors.append('scopes must be a non-empty list')

return errors
Comment thread
rootflo-hardik marked this conversation as resolved.


def validate_email_password_config(config: Dict[str, Any]) -> List[str]:
"""Validate email/password configuration and return list of errors."""
errors = []
Expand Down Expand Up @@ -130,6 +167,23 @@ def get_config_template(auth_type: str) -> Dict[str, Any]:
'response_type': 'code',
'response_mode': 'query',
},
'microsoft_adfs': {
'client_id': 'YOUR_ADFS_CLIENT_ID',
'client_secret': 'YOUR_ADFS_CLIENT_SECRET',
'authority': 'https://fs.your-domain.com',
'redirect_uri': 'https://your-domain.com/v1/oauth/adfs/callback',
'client_redirect_success_url': 'https://your-domain.com/login/success',
'client_redirect_failure_url': 'https://your-domain.com/login/failed',
'scopes': ['openid', 'profile', 'email'],
'response_type': 'code',
'response_mode': 'query',
'authorize_path': '/adfs/oauth2/authorize',
'token_path': '/adfs/oauth2/token',
'jwks_path': '/adfs/discovery/keys',
'expected_issuer': 'https://fs.your-domain.com/adfs',
'clock_skew_seconds': 60,
'verify_ssl': True,
},
}

return templates.get(auth_type, {})
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'/floware/v1/plugin-auth/authenticate',
'/floware/v1/oauth/google/callback',
'/floware/v1/oauth/microsoft/callback',
'/floware/v1/oauth/adfs/callback',
'/floware/v1/plugin-auth/oauth/init',
'/floware/v1/settings/config',
]
Expand Down
Loading
Loading