auth route created for email/password login and closed requests reverified

This commit is contained in:
laxmanhalaki 2025-12-05 10:31:02 +05:30
parent 8c6e71bbb1
commit 17485a9cca
12 changed files with 560 additions and 21 deletions

View File

@ -50,6 +50,47 @@
{
"name": "Authentication",
"item": [
{
"name": "Login with Username/Password",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n // Okta username (email)\n \"username\": \"user@royalenfield.com\",\n \n // Okta password\n \"password\": \"YourOktaPassword123\"\n}"
},
"url": {
"raw": "{{baseUrl}}/auth/login",
"host": ["{{baseUrl}}"],
"path": ["auth", "login"]
},
"description": "Authenticate with username (Okta email) and password.\n\nFlow:\n1. Validates credentials against Okta\n2. If user exists in Okta but not in our DB: creates user automatically\n3. Returns JWT access token and refresh token\n\nPerfect for:\n- Postman testing\n- Mobile apps\n- API clients\n- Development/testing\n\nResponse includes:\n- User profile (created if didn't exist)\n- Access token (24hr validity)\n- Refresh token (7 days validity)"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"// Auto-save access token for subsequent requests",
"if (pm.response.code === 200) {",
" var jsonData = pm.response.json();",
" if (jsonData.data && jsonData.data.accessToken) {",
" pm.collectionVariables.set('accessToken', jsonData.data.accessToken);",
" console.log('✅ Access token saved to collection variable');",
" }",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Token Exchange (Development)",
"request": {

View File

@ -1,2 +1,2 @@
import{a as t}from"./index-onQ3Ui-x.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-IyYM8dvF.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
//# sourceMappingURL=conclusionApi-nj0Kn1y4.js.map
import{a as t}from"./index-DFT-RG6h.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-IyYM8dvF.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
//# sourceMappingURL=conclusionApi-XjRwt26u.js.map

View File

@ -1 +1 @@
{"version":3,"file":"conclusionApi-nj0Kn1y4.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
{"version":3,"file":"conclusionApi-XjRwt26u.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -52,7 +52,7 @@
transition: transform 0.2s ease;
}
</style>
<script type="module" crossorigin src="/assets/index-onQ3Ui-x.js"></script>
<script type="module" crossorigin src="/assets/index-DFT-RG6h.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">

View File

@ -0,0 +1,254 @@
# Username/Password Authentication Endpoint
## Overview
This endpoint allows users to authenticate using their Okta username (email) and password directly via API, without any browser redirects. Perfect for testing with Postman, mobile apps, or other API clients.
## Endpoint Details
**URL:** `POST /api/v1/auth/login`
**Authentication Required:** No
**Content-Type:** `application/json`
## How It Works
1. **Client sends credentials** → Backend validates with Okta
2. **Okta authenticates** → Returns access token
3. **Backend fetches user info** from Okta
4. **User exists in DB?**
- ✅ Yes → Update user info and last login
- ❌ No → **Create new user** in database (like spectator/approver flow)
5. **Return JWT tokens** → Access token + Refresh token
## Request Body
```json
{
"username": "user@example.com",
"password": "YourOktaPassword123"
}
```
### Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `username` | string | Yes | User's Okta email/username |
| `password` | string | Yes | User's Okta password |
## Response
### Success Response (200 OK)
```json
{
"success": true,
"message": "Login successful",
"data": {
"user": {
"userId": "123e4567-e89b-12d3-a456-426614174000",
"employeeId": "EMP001",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"displayName": "John Doe",
"department": "Engineering",
"designation": "Senior Developer",
"role": "USER"
},
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
},
"timestamp": "2024-01-15T10:30:00.000Z"
}
```
### Error Response (401 Unauthorized)
```json
{
"success": false,
"error": "Login failed",
"message": "Authentication failed: Invalid username or password",
"timestamp": "2024-01-15T10:30:00.000Z"
}
```
## Postman Testing
### Step 1: Create New Request
1. Open Postman
2. Create new request: **POST**
3. URL: `http://localhost:5000/api/v1/auth/login`
- Or your backend URL (e.g., `https://re-workflow-nt-dev.siplsolutions.com/api/v1/auth/login`)
### Step 2: Set Headers
```
Content-Type: application/json
```
### Step 3: Set Request Body
Choose **Body****raw** → **JSON**
```json
{
"username": "your-okta-email@example.com",
"password": "your-okta-password"
}
```
### Step 4: Send Request
Click **Send** and you should receive:
- ✅ User object with details
- ✅ Access token (valid for 24 hours)
- ✅ Refresh token (valid for 7 days)
### Step 5: Use Access Token
Copy the `accessToken` from the response and use it in subsequent API calls:
**Headers:**
```
Authorization: Bearer <your-access-token>
```
## User Creation Logic
### Scenario 1: User Exists in Okta & Our DB
- ✅ User authenticated
- ✅ User info updated (department, designation, last login, etc.)
- ✅ Tokens returned
### Scenario 2: User Exists in Okta but NOT in Our DB
- ✅ User authenticated with Okta
- ✅ **New user created** in our database with info from Okta:
- `oktaSub` (Okta subject ID)
- `email` (primary identifier)
- `employeeId` (if available in Okta profile)
- `firstName`, `lastName`, `displayName`
- `department`, `designation`, `phone`
- `role` = "USER" (default)
- `isActive` = true
- ✅ Tokens returned
This is the **same behavior** as when adding a spectator or approver for the first time!
### Scenario 3: User Does NOT Exist in Okta
- ❌ Authentication fails
- ❌ Error: "Invalid username or password"
- ❌ No user created in our DB
## Important Notes
### 🔐 Security
1. **HTTPS Required in Production**: Always use HTTPS to protect credentials in transit
2. **Rate Limiting**: Consider adding rate limiting to prevent brute force attacks
3. **Okta Password Policy**: Follows Okta's password complexity requirements
### ⚙️ Okta Configuration Required
This endpoint uses **Resource Owner Password Credentials** grant type. Your Okta application must have this enabled:
1. Go to Okta Admin Console
2. Navigate to **Applications** → Your Application
3. Under **General Settings** → **Grant Types**
4. Enable: ✅ **Resource Owner Password**
### 📝 Token Management
**Access Token:**
- Valid for: 24 hours
- Used for: API authentication
- Header: `Authorization: Bearer <token>`
**Refresh Token:**
- Valid for: 7 days
- Used for: Getting new access tokens
- Endpoint: `POST /api/v1/auth/refresh`
## Example Postman Collection
### 1. Login
```http
POST http://localhost:5000/api/v1/auth/login
Content-Type: application/json
{
"username": "john.doe@royalenfield.com",
"password": "SecurePassword123!"
}
```
### 2. Get Current User (Protected Route)
```http
GET http://localhost:5000/api/v1/auth/me
Authorization: Bearer <access-token-from-login>
```
### 3. Refresh Access Token
```http
POST http://localhost:5000/api/v1/auth/refresh
Content-Type: application/json
{
"refreshToken": "<refresh-token-from-login>"
}
```
## Troubleshooting
### Error: "OKTA_CLIENT_SECRET is not configured"
**Solution:** Check your `.env` file has valid Okta credentials:
```env
OKTA_DOMAIN=https://dev-xxxxx.okta.com
OKTA_CLIENT_ID=your_client_id
OKTA_CLIENT_SECRET=your_client_secret
```
### Error: "Authentication failed: invalid_grant"
**Possible causes:**
1. Username or password is incorrect
2. User account is locked/suspended in Okta
3. Resource Owner Password grant not enabled in Okta
### Error: "Authentication failed: invalid_client"
**Solution:** Verify `OKTA_CLIENT_ID` and `OKTA_CLIENT_SECRET` are correct
### New User Not Created
**Check logs:** Backend logs will show if user creation failed and why
```bash
npm run dev # Check console output
```
## Comparison: SSO vs Password Login
| Feature | SSO Flow (Browser) | Password Login (API) |
|---------|-------------------|---------------------|
| **Use Case** | Web application login | Postman, Mobile apps, API clients |
| **User Experience** | Browser redirect to Okta | Direct username/password |
| **Security** | OAuth 2.0 Authorization Code | Resource Owner Password |
| **Best For** | Production web apps | Testing, Mobile apps, Internal tools |
| **Endpoint** | `/api/v1/auth/token-exchange` | `/api/v1/auth/login` |
## Next Steps
After successful authentication:
1. Store the `accessToken` securely
2. Use it in `Authorization` header for protected endpoints
3. Refresh it using `refreshToken` before expiry
4. Call `/api/v1/auth/logout` when user logs out
## Related Endpoints
- `POST /api/v1/auth/refresh` - Refresh access token
- `GET /api/v1/auth/me` - Get current user profile
- `GET /api/v1/auth/validate` - Validate current token
- `POST /api/v1/auth/logout` - Logout and clear cookies

View File

@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import { AuthService } from '../services/auth.service';
import { validateSSOCallback, validateRefreshToken, validateTokenExchange } from '../validators/auth.validator';
import { validateSSOCallback, validateRefreshToken, validateTokenExchange, validatePasswordLogin } from '../validators/auth.validator';
import { ResponseHandler } from '../utils/responseHandler';
import type { AuthenticatedRequest } from '../types/express';
import logger from '../utils/logger';
@ -299,6 +299,87 @@ export class AuthController {
}
}
/**
* Login with username and password
* POST /api/v1/auth/login
*
* This endpoint:
* 1. Validates credentials against Okta
* 2. Creates user in DB if they exist in Okta but not in our DB
* 3. Returns JWT access and refresh tokens
*/
async login(req: Request, res: Response): Promise<void> {
try {
logger.info('Password login request received', {
username: req.body?.username,
hasPassword: !!req.body?.password,
});
const { username, password } = validatePasswordLogin(req.body);
const result = await this.authService.authenticateWithPassword(username, password);
// Log login activity
const requestMeta = getRequestMetadata(req);
await activityService.log({
requestId: SYSTEM_EVENT_REQUEST_ID,
type: 'login',
user: {
userId: result.user.userId,
name: result.user.displayName || result.user.email,
email: result.user.email
},
timestamp: new Date().toISOString(),
action: 'User Login',
details: `User logged in via username/password from ${requestMeta.ipAddress || 'unknown IP'}`,
metadata: {
loginMethod: 'PASSWORD',
employeeId: result.user.employeeId,
department: result.user.department,
role: result.user.role
},
ipAddress: requestMeta.ipAddress,
userAgent: requestMeta.userAgent,
category: 'AUTHENTICATION',
severity: 'INFO'
});
// Set cookies for web clients
const isProduction = process.env.NODE_ENV === 'production';
const cookieOptions = {
httpOnly: true,
secure: isProduction,
sameSite: isProduction ? 'none' as const : 'lax' as const,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
};
res.cookie('accessToken', result.accessToken, cookieOptions);
const refreshCookieOptions = {
...cookieOptions,
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
};
res.cookie('refreshToken', result.refreshToken, refreshCookieOptions);
logger.info('Password login successful', {
userId: result.user.userId,
email: result.user.email,
});
// Return tokens in response (for Postman/API clients)
ResponseHandler.success(res, {
user: result.user,
accessToken: result.accessToken,
refreshToken: result.refreshToken,
}, 'Login successful');
} catch (error) {
logger.error('Password login failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Invalid credentials';
ResponseHandler.error(res, 'Login failed', 401, errorMessage);
}
}
/**
* Exchange authorization code for tokens
* POST /api/v1/auth/token-exchange

View File

@ -2,12 +2,18 @@ import { Router, Request, Response, NextFunction } from 'express';
import { AuthController } from '../controllers/auth.controller';
import { authenticateToken } from '../middlewares/auth.middleware';
import { validateBody } from '../middlewares/validate.middleware';
import { ssoCallbackSchema, refreshTokenSchema, tokenExchangeSchema } from '../validators/auth.validator';
import { ssoCallbackSchema, refreshTokenSchema, tokenExchangeSchema, passwordLoginSchema } from '../validators/auth.validator';
import { asyncHandler } from '../middlewares/errorHandler.middleware';
const router = Router();
const authController = new AuthController();
// Username/Password login endpoint (no authentication required) - for direct API access
router.post('/login',
validateBody(passwordLoginSchema),
asyncHandler(authController.login.bind(authController))
);
// Token exchange endpoint (no authentication required) - for localhost development
router.post('/token-exchange',
validateBody(tokenExchangeSchema),

View File

@ -240,6 +240,154 @@ export class AuthService {
}
}
/**
* Authenticate user with username (email) and password via Okta API
* This is for direct API authentication (e.g., Postman, mobile apps)
*
* Flow:
* 1. Authenticate with Okta using username/password
* 2. Get access token from Okta
* 3. Fetch user info from Okta
* 4. Create/update user in our database if needed
* 5. Return our JWT tokens
*/
async authenticateWithPassword(username: string, password: string): Promise<LoginResponse> {
try {
logger.info('Authenticating user with username/password', { username });
// Step 1: Authenticate with Okta using Resource Owner Password flow
// Note: This requires Okta to have Resource Owner Password grant type enabled
const tokenEndpoint = `${ssoConfig.oktaDomain}/oauth2/default/v1/token`;
const tokenResponse = await axios.post(
tokenEndpoint,
new URLSearchParams({
grant_type: 'password',
username: username,
password: password,
scope: 'openid profile email',
client_id: ssoConfig.oktaClientId,
client_secret: ssoConfig.oktaClientSecret,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
validateStatus: (status) => status < 500,
}
);
// Check for authentication errors
if (tokenResponse.status !== 200) {
logger.error('Okta authentication failed', {
status: tokenResponse.status,
data: tokenResponse.data,
});
const errorData = tokenResponse.data || {};
const errorMessage = errorData.error_description || errorData.error || 'Invalid username or password';
throw new Error(`Authentication failed: ${errorMessage}`);
}
const { access_token, refresh_token, id_token } = tokenResponse.data;
if (!access_token) {
throw new Error('Failed to obtain access token from Okta');
}
logger.info('Successfully authenticated with Okta');
// Step 2: Get user info from Okta
const userInfoEndpoint = `${ssoConfig.oktaDomain}/oauth2/default/v1/userinfo`;
const userInfoResponse = await axios.get(userInfoEndpoint, {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
const oktaUser = userInfoResponse.data;
// Step 3: Extract user data from Okta response
const oktaSub = oktaUser.sub || '';
if (!oktaSub) {
throw new Error('Okta sub (subject identifier) not found in response');
}
const employeeId =
oktaUser.employeeId ||
oktaUser.employee_id ||
oktaUser.empId ||
oktaUser.employeeNumber ||
undefined;
const userData: SSOUserData = {
oktaSub: oktaSub,
email: oktaUser.email || username,
employeeId: employeeId,
};
// Add optional fields
if (oktaUser.given_name || oktaUser.firstName) {
userData.firstName = oktaUser.given_name || oktaUser.firstName;
}
if (oktaUser.family_name || oktaUser.lastName) {
userData.lastName = oktaUser.family_name || oktaUser.lastName;
}
if (oktaUser.name) {
userData.displayName = oktaUser.name;
}
if (oktaUser.department) {
userData.department = oktaUser.department;
}
if (oktaUser.title || oktaUser.designation) {
userData.designation = oktaUser.title || oktaUser.designation;
}
if (oktaUser.phone_number || oktaUser.phone) {
userData.phone = oktaUser.phone_number || oktaUser.phone;
}
logger.info('User data extracted from Okta', {
email: userData.email,
hasEmployeeId: !!userData.employeeId,
hasName: !!userData.displayName,
});
// Step 4: Create/update user in our database
const result = await this.handleSSOCallback(userData);
logger.info('User authenticated successfully via password flow', {
userId: result.user.userId,
email: result.user.email,
});
// Return tokens (including Okta tokens for reference)
return {
...result,
oktaRefreshToken: refresh_token,
oktaAccessToken: access_token,
oktaIdToken: id_token,
};
} catch (error: any) {
logger.error('Password authentication failed', {
username,
error: error.message,
status: error.response?.status,
oktaError: error.response?.data,
});
if (error.response?.data) {
const errorData = error.response.data;
if (typeof errorData === 'object' && !Array.isArray(errorData)) {
const errorMsg = errorData.error_description || errorData.error || error.message;
throw new Error(`Authentication failed: ${errorMsg}`);
}
}
throw new Error(`Authentication failed: ${error.message || 'Invalid credentials'}`);
}
}
/**
* Exchange authorization code for tokens with Okta/Auth0
*

View File

@ -22,6 +22,11 @@ export const tokenExchangeSchema = z.object({
redirectUri: z.string().url('Valid redirect URI is required'),
});
export const passwordLoginSchema = z.object({
username: z.string().email('Valid email/username is required').or(z.string().min(1, 'Username is required')),
password: z.string().min(1, 'Password is required'),
});
export const validateSSOCallback = (data: any) => {
return ssoCallbackSchema.parse(data);
};
@ -33,3 +38,7 @@ export const validateRefreshToken = (data: any) => {
export const validateTokenExchange = (data: any) => {
return tokenExchangeSchema.parse(data);
};
export const validatePasswordLogin = (data: any) => {
return passwordLoginSchema.parse(data);
};