-
This commit is contained in:
@ -49,39 +49,46 @@ func (hv *HeaderValidator) ValidateAuthenticationHeaders(r *http.Request) (*Vali
|
|||||||
return nil, errors.NewAuthenticationError("User authentication required")
|
return nil, errors.NewAuthenticationError("User authentication required")
|
||||||
}
|
}
|
||||||
|
|
||||||
if timestamp == "" || signature == "" {
|
// In development mode, skip signature validation for trusted headers
|
||||||
hv.logger.Warn("Missing authentication signature headers",
|
if hv.config.IsDevelopment() {
|
||||||
|
hv.logger.Debug("Development mode: skipping signature validation",
|
||||||
zap.String("user_email", userEmail))
|
zap.String("user_email", userEmail))
|
||||||
return nil, errors.NewAuthenticationError("Authentication signature required")
|
} else {
|
||||||
}
|
// Production mode: require full signature validation
|
||||||
|
if timestamp == "" || signature == "" {
|
||||||
|
hv.logger.Warn("Missing authentication signature headers",
|
||||||
|
zap.String("user_email", userEmail))
|
||||||
|
return nil, errors.NewAuthenticationError("Authentication signature required")
|
||||||
|
}
|
||||||
|
|
||||||
// Validate timestamp (prevent replay attacks)
|
// Validate timestamp (prevent replay attacks)
|
||||||
timestampInt, err := strconv.ParseInt(timestamp, 10, 64)
|
timestampInt, err := strconv.ParseInt(timestamp, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hv.logger.Warn("Invalid timestamp format",
|
hv.logger.Warn("Invalid timestamp format",
|
||||||
zap.String("timestamp", timestamp),
|
zap.String("timestamp", timestamp),
|
||||||
zap.String("user_email", userEmail))
|
zap.String("user_email", userEmail))
|
||||||
return nil, errors.NewAuthenticationError("Invalid timestamp format")
|
return nil, errors.NewAuthenticationError("Invalid timestamp format")
|
||||||
}
|
}
|
||||||
|
|
||||||
timestampTime := time.Unix(timestampInt, 0)
|
timestampTime := time.Unix(timestampInt, 0)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
// Allow 5 minutes clock skew
|
// Allow 5 minutes clock skew
|
||||||
maxAge := 5 * time.Minute
|
maxAge := 5 * time.Minute
|
||||||
if now.Sub(timestampTime) > maxAge || timestampTime.After(now.Add(1*time.Minute)) {
|
if now.Sub(timestampTime) > maxAge || timestampTime.After(now.Add(1*time.Minute)) {
|
||||||
hv.logger.Warn("Timestamp outside acceptable window",
|
hv.logger.Warn("Timestamp outside acceptable window",
|
||||||
zap.Time("timestamp", timestampTime),
|
zap.Time("timestamp", timestampTime),
|
||||||
zap.Time("now", now),
|
zap.Time("now", now),
|
||||||
zap.String("user_email", userEmail))
|
zap.String("user_email", userEmail))
|
||||||
return nil, errors.NewAuthenticationError("Request timestamp outside acceptable window")
|
return nil, errors.NewAuthenticationError("Request timestamp outside acceptable window")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate HMAC signature
|
// Validate HMAC signature
|
||||||
if !hv.validateSignature(userEmail, timestamp, signature) {
|
if !hv.validateSignature(userEmail, timestamp, signature) {
|
||||||
hv.logger.Warn("Invalid authentication signature",
|
hv.logger.Warn("Invalid authentication signature",
|
||||||
zap.String("user_email", userEmail))
|
zap.String("user_email", userEmail))
|
||||||
return nil, errors.NewAuthenticationError("Invalid authentication signature")
|
return nil, errors.NewAuthenticationError("Invalid authentication signature")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email format
|
// Validate email format
|
||||||
@ -94,11 +101,24 @@ func (hv *HeaderValidator) ValidateAuthenticationHeaders(r *http.Request) (*Vali
|
|||||||
hv.logger.Debug("Authentication headers validated successfully",
|
hv.logger.Debug("Authentication headers validated successfully",
|
||||||
zap.String("user_email", userEmail))
|
zap.String("user_email", userEmail))
|
||||||
|
|
||||||
|
// Set defaults for development mode
|
||||||
|
var timestampTime time.Time
|
||||||
|
var signatureValue string
|
||||||
|
|
||||||
|
if hv.config.IsDevelopment() {
|
||||||
|
timestampTime = time.Now()
|
||||||
|
signatureValue = "dev-mode-bypass"
|
||||||
|
} else {
|
||||||
|
timestampInt, _ := strconv.ParseInt(timestamp, 10, 64)
|
||||||
|
timestampTime = time.Unix(timestampInt, 0)
|
||||||
|
signatureValue = signature
|
||||||
|
}
|
||||||
|
|
||||||
return &ValidatedUserContext{
|
return &ValidatedUserContext{
|
||||||
UserID: userEmail,
|
UserID: userEmail,
|
||||||
Email: userEmail,
|
Email: userEmail,
|
||||||
Timestamp: timestampTime,
|
Timestamp: timestampTime,
|
||||||
Signature: signature,
|
Signature: signatureValue,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -124,11 +124,20 @@ type VerifyResponse struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TokenDeliveryMode specifies how tokens should be delivered in redirect flows
|
||||||
|
type TokenDeliveryMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenDeliveryCookie TokenDeliveryMode = "cookie" // Token in secure cookie (default)
|
||||||
|
TokenDeliveryQuery TokenDeliveryMode = "query" // Token in query parameter (for integrations)
|
||||||
|
)
|
||||||
|
|
||||||
// LoginRequest represents a user login request
|
// LoginRequest represents a user login request
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
AppID string `json:"app_id" validate:"required"`
|
AppID string `json:"app_id" validate:"required"`
|
||||||
Permissions []string `json:"permissions,omitempty"`
|
Permissions []string `json:"permissions,omitempty"`
|
||||||
RedirectURI string `json:"redirect_uri,omitempty"`
|
RedirectURI string `json:"redirect_uri,omitempty"`
|
||||||
|
TokenDelivery TokenDeliveryMode `json:"token_delivery,omitempty"` // How to deliver token in redirect flows
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginResponse represents a user login response
|
// LoginResponse represents a user login response
|
||||||
|
|||||||
@ -81,25 +81,53 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For redirect flows, use secure cookie-based token delivery
|
// For redirect flows, choose token delivery method
|
||||||
// Set secure cookie with the token
|
// Default to cookie delivery for security
|
||||||
c.SetSameSite(http.SameSiteStrictMode)
|
tokenDelivery := req.TokenDelivery
|
||||||
c.SetCookie(
|
if tokenDelivery == "" {
|
||||||
"auth_token", // name
|
tokenDelivery = domain.TokenDeliveryCookie
|
||||||
token, // value
|
}
|
||||||
604800, // maxAge (7 days)
|
|
||||||
"/", // path
|
h.logger.Debug("Token delivery mode", zap.String("mode", string(tokenDelivery)))
|
||||||
"", // domain (empty for current domain)
|
|
||||||
true, // secure (HTTPS only)
|
|
||||||
true, // httpOnly (no JavaScript access)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generate a secure state parameter for CSRF protection
|
// Generate a secure state parameter for CSRF protection
|
||||||
state := h.generateSecureState(userContext.UserID, req.AppID)
|
state := h.generateSecureState(userContext.UserID, req.AppID)
|
||||||
|
var redirectURL string
|
||||||
|
|
||||||
|
switch tokenDelivery {
|
||||||
|
case domain.TokenDeliveryQuery:
|
||||||
|
// Deliver token via query parameter (for integrations like VS Code)
|
||||||
|
redirectURL = req.RedirectURI + "?token=" + token + "&state=" + state
|
||||||
|
|
||||||
|
case domain.TokenDeliveryCookie:
|
||||||
|
// Deliver token via secure cookie (default, more secure)
|
||||||
|
c.SetSameSite(http.SameSiteStrictMode)
|
||||||
|
|
||||||
|
// In development mode, make cookie accessible to JavaScript for testing
|
||||||
|
// In production, keep HTTP-only for security
|
||||||
|
httpOnly := !h.config.IsDevelopment()
|
||||||
|
secure := !h.config.IsDevelopment() // Only require HTTPS in production
|
||||||
|
|
||||||
|
c.SetCookie(
|
||||||
|
"auth_token", // name
|
||||||
|
token, // value
|
||||||
|
604800, // maxAge (7 days)
|
||||||
|
"/", // path
|
||||||
|
"", // domain (empty for current domain)
|
||||||
|
secure, // secure (HTTPS only in production)
|
||||||
|
httpOnly, // httpOnly (no JavaScript access in production)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Redirect without token in URL for security
|
||||||
|
redirectURL = req.RedirectURI + "?state=" + state
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Invalid delivery mode, default to cookie
|
||||||
|
redirectURL = req.RedirectURI + "?state=" + state
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect without token in URL
|
|
||||||
response := domain.LoginResponse{
|
response := domain.LoginResponse{
|
||||||
RedirectURL: req.RedirectURI + "?state=" + state,
|
RedirectURL: redirectURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, response)
|
c.JSON(http.StatusOK, response)
|
||||||
|
|||||||
@ -47,7 +47,13 @@ interface CallbackTestResult {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
token?: string;
|
token?: string;
|
||||||
verified?: boolean;
|
verified?: boolean;
|
||||||
|
permitted?: boolean;
|
||||||
|
user_id?: string;
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
|
permission_results?: Record<string, boolean>;
|
||||||
|
expires_at?: string;
|
||||||
|
max_valid_at?: string;
|
||||||
|
token_type?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
@ -79,6 +85,8 @@ const TokenTester: React.FC = () => {
|
|||||||
const [callbackModalVisible, setCallbackModalVisible] = useState(false);
|
const [callbackModalVisible, setCallbackModalVisible] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [callbackForm] = Form.useForm();
|
const [callbackForm] = Form.useForm();
|
||||||
|
const [useCallback, setUseCallback] = useState(false);
|
||||||
|
const [extractedToken, setExtractedToken] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadApplications();
|
loadApplications();
|
||||||
@ -123,7 +131,8 @@ const TokenTester: React.FC = () => {
|
|||||||
const response = await apiService.login(
|
const response = await apiService.login(
|
||||||
values.app_id,
|
values.app_id,
|
||||||
values.permissions || [],
|
values.permissions || [],
|
||||||
values.use_callback ? callbackUrl : undefined
|
values.use_callback ? callbackUrl : undefined,
|
||||||
|
values.token_delivery || 'query'
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('Login response:', response);
|
console.log('Login response:', response);
|
||||||
@ -146,16 +155,25 @@ const TokenTester: React.FC = () => {
|
|||||||
// If we have a redirect URL, show the callback modal
|
// If we have a redirect URL, show the callback modal
|
||||||
if (response.redirect_url && values.use_callback) {
|
if (response.redirect_url && values.use_callback) {
|
||||||
setCallbackModalVisible(true);
|
setCallbackModalVisible(true);
|
||||||
// Pre-fill the callback form with the token from the redirect URL
|
|
||||||
const urlParams = new URLSearchParams(response.redirect_url.split('?')[1]);
|
// Extract token from redirect URL if using query parameter delivery
|
||||||
const token = urlParams.get('token');
|
let tokenFromUrl = '';
|
||||||
if (token) {
|
if (values.token_delivery === 'query') {
|
||||||
callbackForm.setFieldsValue({
|
try {
|
||||||
app_id: values.app_id,
|
const url = new URL(response.redirect_url);
|
||||||
token: token,
|
tokenFromUrl = url.searchParams.get('token') || '';
|
||||||
permissions: values.permissions || [],
|
} catch (e) {
|
||||||
});
|
console.warn('Failed to parse redirect URL for token extraction:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setExtractedToken(tokenFromUrl);
|
||||||
|
|
||||||
|
callbackForm.setFieldsValue({
|
||||||
|
app_id: values.app_id,
|
||||||
|
token: tokenFromUrl, // Pre-fill with extracted token if available
|
||||||
|
permissions: values.permissions || [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -195,7 +213,13 @@ const TokenTester: React.FC = () => {
|
|||||||
success: verifyResponse.valid,
|
success: verifyResponse.valid,
|
||||||
token: values.token,
|
token: values.token,
|
||||||
verified: verifyResponse.valid,
|
verified: verifyResponse.valid,
|
||||||
|
permitted: verifyResponse.permitted,
|
||||||
|
user_id: verifyResponse.user_id,
|
||||||
permissions: verifyResponse.permissions,
|
permissions: verifyResponse.permissions,
|
||||||
|
permission_results: verifyResponse.permission_results,
|
||||||
|
expires_at: verifyResponse.expires_at,
|
||||||
|
max_valid_at: verifyResponse.max_valid_at,
|
||||||
|
token_type: verifyResponse.token_type,
|
||||||
error: verifyResponse.error,
|
error: verifyResponse.error,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@ -205,8 +229,16 @@ const TokenTester: React.FC = () => {
|
|||||||
|
|
||||||
if (verifyResponse.valid) {
|
if (verifyResponse.valid) {
|
||||||
message.success('Callback test completed successfully!');
|
message.success('Callback test completed successfully!');
|
||||||
|
// Auto-close modal after successful verification to show results
|
||||||
|
setTimeout(() => {
|
||||||
|
setCallbackModalVisible(false);
|
||||||
|
}, 1500);
|
||||||
} else {
|
} else {
|
||||||
message.warning('Callback test completed - token verification failed');
|
message.warning('Callback test completed - token verification failed');
|
||||||
|
// Auto-close modal after failed verification to show results
|
||||||
|
setTimeout(() => {
|
||||||
|
setCallbackModalVisible(false);
|
||||||
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -221,6 +253,11 @@ const TokenTester: React.FC = () => {
|
|||||||
setCallbackResult(result);
|
setCallbackResult(result);
|
||||||
setCurrentStep(4);
|
setCurrentStep(4);
|
||||||
message.error('Callback test failed');
|
message.error('Callback test failed');
|
||||||
|
|
||||||
|
// Auto-close modal to show error results
|
||||||
|
setTimeout(() => {
|
||||||
|
setCallbackModalVisible(false);
|
||||||
|
}, 1500);
|
||||||
} finally {
|
} finally {
|
||||||
setCallbackLoading(false);
|
setCallbackLoading(false);
|
||||||
}
|
}
|
||||||
@ -231,6 +268,8 @@ const TokenTester: React.FC = () => {
|
|||||||
setLoginResult(null);
|
setLoginResult(null);
|
||||||
setCallbackResult(null);
|
setCallbackResult(null);
|
||||||
setCallbackModalVisible(false);
|
setCallbackModalVisible(false);
|
||||||
|
setUseCallback(false);
|
||||||
|
setExtractedToken('');
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
callbackForm.resetFields();
|
callbackForm.resetFields();
|
||||||
// Clear stored test data
|
// Clear stored test data
|
||||||
@ -257,6 +296,20 @@ const TokenTester: React.FC = () => {
|
|||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
Test the /login flow and callback handling for user tokens
|
Test the /login flow and callback handling for user tokens
|
||||||
</Text>
|
</Text>
|
||||||
|
<div style={{ marginTop: '8px' }}>
|
||||||
|
<Alert
|
||||||
|
message="Two Testing Modes Available"
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
<Text strong>Direct Mode:</Text> Login returns token directly in response body (no callback)<br/>
|
||||||
|
<Text strong>Callback Mode:</Text> Login returns redirect URL, token in query parameter (default) or secure cookie
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
@ -316,11 +369,44 @@ const TokenTester: React.FC = () => {
|
|||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
label=" "
|
label=" "
|
||||||
>
|
>
|
||||||
<Checkbox>Use callback URL (test full flow)</Checkbox>
|
<Checkbox onChange={(e) => setUseCallback(e.target.checked)}>
|
||||||
|
Use callback URL (test full flow)
|
||||||
|
</Checkbox>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="token_delivery"
|
||||||
|
label="Token Delivery Method (for callback flows)"
|
||||||
|
tooltip="Choose how tokens are delivered when using callback URLs"
|
||||||
|
initialValue="query"
|
||||||
|
>
|
||||||
|
<Select placeholder="Select delivery method" disabled={!useCallback} defaultValue="query">
|
||||||
|
<Option value="query">
|
||||||
|
<div>
|
||||||
|
<Text strong>Query Parameter</Text> (Recommended for testing)
|
||||||
|
<br />
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
|
Token included in callback URL query string
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
<Option value="cookie">
|
||||||
|
<div>
|
||||||
|
<Text strong>Cookie</Text> (More secure for production)
|
||||||
|
<br />
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
|
Token stored in HTTP-only cookie
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="permissions"
|
name="permissions"
|
||||||
label="Permissions to Request"
|
label="Permissions to Request"
|
||||||
@ -480,20 +566,83 @@ const TokenTester: React.FC = () => {
|
|||||||
showIcon
|
showIcon
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{callbackResult.success && callbackResult.permissions && (
|
{callbackResult.success && (
|
||||||
<div>
|
<div>
|
||||||
<Text strong>Verified Permissions:</Text>
|
<Row gutter={16}>
|
||||||
<div style={{ marginTop: '8px' }}>
|
<Col span={12}>
|
||||||
{callbackResult.permissions.map(permission => (
|
<Card size="small" title="Token Information">
|
||||||
<Tag key={permission} color="green" style={{ margin: '2px' }}>
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
{permission}
|
<div>
|
||||||
</Tag>
|
<Text strong>Token Type:</Text>
|
||||||
))}
|
<div>
|
||||||
</div>
|
<Tag color="blue">{(callbackResult.token_type || 'user').toUpperCase()}</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{callbackResult.user_id && (
|
||||||
|
<div>
|
||||||
|
<Text strong>User ID:</Text>
|
||||||
|
<div>{callbackResult.user_id}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{callbackResult.expires_at && (
|
||||||
|
<div>
|
||||||
|
<Text strong>Expires At:</Text>
|
||||||
|
<div>{new Date(callbackResult.expires_at).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{callbackResult.max_valid_at && (
|
||||||
|
<div>
|
||||||
|
<Text strong>Max Valid Until:</Text>
|
||||||
|
<div>{new Date(callbackResult.max_valid_at).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={12}>
|
||||||
|
<Card size="small" title="Permissions">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{callbackResult.permissions && callbackResult.permissions.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<Text strong>Available Permissions:</Text>
|
||||||
|
<div style={{ marginTop: '8px' }}>
|
||||||
|
{callbackResult.permissions.map(permission => (
|
||||||
|
<Tag key={permission} color="green" style={{ margin: '2px' }}>
|
||||||
|
{permission}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">No permissions available</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{callbackResult.permission_results && Object.keys(callbackResult.permission_results).length > 0 && (
|
||||||
|
<div style={{ marginTop: '16px' }}>
|
||||||
|
<Text strong>Permission Check Results:</Text>
|
||||||
|
<div style={{ marginTop: '8px' }}>
|
||||||
|
{Object.entries(callbackResult.permission_results).map(([permission, granted]) => (
|
||||||
|
<div key={permission} style={{ marginBottom: '4px' }}>
|
||||||
|
<Tag color={granted ? 'green' : 'red'}>
|
||||||
|
{permission}: {granted ? 'GRANTED' : 'DENIED'}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div style={{ marginTop: '16px' }}>
|
||||||
<Text strong>Timestamp:</Text>
|
<Text strong>Timestamp:</Text>
|
||||||
<div>{new Date(callbackResult.timestamp).toLocaleString()}</div>
|
<div>{new Date(callbackResult.timestamp).toLocaleString()}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -514,7 +663,10 @@ const TokenTester: React.FC = () => {
|
|||||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
<Alert
|
<Alert
|
||||||
message="Callback URL Received"
|
message="Callback URL Received"
|
||||||
description="The login flow returned a redirect URL with a token. Test the callback handling by verifying the token."
|
description={extractedToken
|
||||||
|
? "Token successfully extracted from callback URL. Verify the token to complete the flow test."
|
||||||
|
: "Redirect URL received. If using cookie delivery, the token is stored in a secure cookie."
|
||||||
|
}
|
||||||
type="info"
|
type="info"
|
||||||
showIcon
|
showIcon
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -62,8 +62,15 @@ const TokenTesterCallback: React.FC = () => {
|
|||||||
|
|
||||||
// Parse URL parameters
|
// Parse URL parameters
|
||||||
const urlParams = new URLSearchParams(location.search);
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
let token = urlParams.get('token') || undefined;
|
||||||
|
|
||||||
|
// If no token in URL, try to extract from auth_token cookie
|
||||||
|
if (!token) {
|
||||||
|
token = getCookie('auth_token') || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const data: CallbackData = {
|
const data: CallbackData = {
|
||||||
token: urlParams.get('token') || undefined,
|
token: token,
|
||||||
state: urlParams.get('state') || undefined,
|
state: urlParams.get('state') || undefined,
|
||||||
error: urlParams.get('error') || undefined,
|
error: urlParams.get('error') || undefined,
|
||||||
error_description: urlParams.get('error_description') || undefined,
|
error_description: urlParams.get('error_description') || undefined,
|
||||||
@ -84,6 +91,17 @@ const TokenTesterCallback: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Utility function to get cookie value by name
|
||||||
|
const getCookie = (name: string): string | null => {
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const cookieValue = parts.pop()?.split(';').shift();
|
||||||
|
return cookieValue || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const verifyToken = async (token: string) => {
|
const verifyToken = async (token: string) => {
|
||||||
try {
|
try {
|
||||||
// We need to extract app_id from the state or make a best guess
|
// We need to extract app_id from the state or make a best guess
|
||||||
|
|||||||
@ -186,11 +186,12 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
async login(appId: string, permissions: string[], redirectUri?: string): Promise<any> {
|
async login(appId: string, permissions: string[], redirectUri?: string, tokenDelivery?: string): Promise<any> {
|
||||||
const response = await this.api.post('/api/login', {
|
const response = await this.api.post('/api/login', {
|
||||||
app_id: appId,
|
app_id: appId,
|
||||||
permissions,
|
permissions,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
|
token_delivery: tokenDelivery,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ server {
|
|||||||
# Apply rate limiting
|
# Apply rate limiting
|
||||||
limit_req zone=api burst=20 nodelay;
|
limit_req zone=api burst=20 nodelay;
|
||||||
|
|
||||||
# Add test user header for HeaderAuthenticationProvider
|
# Development mode: only user email header required
|
||||||
proxy_set_header X-User-Email "test@example.com";
|
proxy_set_header X-User-Email "test@example.com";
|
||||||
|
|
||||||
# Standard proxy headers
|
# Standard proxy headers
|
||||||
@ -38,7 +38,7 @@ server {
|
|||||||
# Apply stricter rate limiting for auth endpoints
|
# Apply stricter rate limiting for auth endpoints
|
||||||
limit_req zone=login burst=5 nodelay;
|
limit_req zone=login burst=5 nodelay;
|
||||||
|
|
||||||
# Add test user header for HeaderAuthenticationProvider
|
# Development mode: only user email header required
|
||||||
proxy_set_header X-User-Email "test@example.com";
|
proxy_set_header X-User-Email "test@example.com";
|
||||||
|
|
||||||
# Standard proxy headers
|
# Standard proxy headers
|
||||||
@ -47,7 +47,7 @@ server {
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
# Proxy to API service (cannot have URI part in regex location)
|
# Proxy to API service
|
||||||
proxy_pass http://api-service:8080;
|
proxy_pass http://api-service:8080;
|
||||||
proxy_read_timeout 60s;
|
proxy_read_timeout 60s;
|
||||||
proxy_connect_timeout 10s;
|
proxy_connect_timeout 10s;
|
||||||
@ -139,7 +139,7 @@ server {
|
|||||||
location /api/ {
|
location /api/ {
|
||||||
limit_req zone=api burst=50 nodelay;
|
limit_req zone=api burst=50 nodelay;
|
||||||
|
|
||||||
# Admin test user
|
# Development mode: admin test user
|
||||||
proxy_set_header X-User-Email "admin@example.com";
|
proxy_set_header X-User-Email "admin@example.com";
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@ -159,7 +159,7 @@ server {
|
|||||||
location /api/ {
|
location /api/ {
|
||||||
limit_req zone=api burst=10 nodelay;
|
limit_req zone=api burst=10 nodelay;
|
||||||
|
|
||||||
# Limited test user
|
# Development mode: limited test user
|
||||||
proxy_set_header X-User-Email "limited@example.com";
|
proxy_set_header X-User-Email "limited@example.com";
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
Reference in New Issue
Block a user