Skip to content

Commit 8b7e3c6

Browse files
alinarubleaclaude
andauthored
feat: add SkipAuthHandler for local dev authentication bypass (#2181)
## Summary - Adds `SkipAuthHandler` as the first handler in the auth middleware chain - When `SKIP_AUTH=true` (local dev `.env`), injects a mock admin identity and short-circuits auth - When `SKIP_AUTH` is absent or not `'true'` (all deployed environments), returns `null` and the chain falls through to the real handlers — zero production impact - Fixes 401 errors when running the API service locally via `make run-api` in the mysticat-workspace local dev environment ## Context The local dev environment sets `SKIP_AUTH=true` in `.env`, but previously no auth handler recognized this variable. All four real handlers (JWT, IMS, ScopedApiKey, LegacyApiKey) would fail and return 401 because the UI sends no credentials in local dev mode. ## Test plan - [x] Unit tests pass (7791 passing, 1 pre-existing flaky timeout) - [ ] `make run-api` in mysticat-workspace local dev → API responds 200 without auth headers - [ ] Deployed environments unaffected (`SKIP_AUTH` is never set) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e71ddcd commit 8b7e3c6

1 file changed

Lines changed: 34 additions & 17 deletions

File tree

src/index.js

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
s2sAuthWrapper,
3030
} from '@adobe/spacecat-shared-http-utils';
3131
import AuthInfo from '@adobe/spacecat-shared-http-utils/src/auth/auth-info.js';
32+
import AbstractHandler from '@adobe/spacecat-shared-http-utils/src/auth/handlers/abstract.js';
3233
import { imsClientWrapper } from '@adobe/spacecat-shared-ims-client';
3334
import {
3435
elevatedSlackClientWrapper,
@@ -136,36 +137,50 @@ function localCORSWrapper(fn) {
136137
}
137138
/* c8 ignore stop */
138139

140+
/* c8 ignore start */
139141
/**
140-
* This is the main function
141-
* @param {Request} request the request object (see fetch api)
142-
* @param {UniversalContext} context the context of the universal serverless function
143-
* @returns {Response} a response
142+
* Auth handler that bypasses authentication when SKIP_AUTH=true.
143+
* For local development only — injects a mock admin identity.
144144
*/
145-
async function run(request, context) {
146-
const { log, pathInfo, env } = context;
147-
const { route, suffix, method } = pathInfo;
145+
class SkipAuthHandler extends AbstractHandler {
146+
constructor(log) {
147+
super('skipAuth', log);
148+
}
148149

149-
// Add mock authInfo when authentication is skipped
150-
/* c8 ignore start */
151-
if (env.SKIP_AUTH === 'true' && !context.attributes?.authInfo) {
152-
if (!context.attributes) {
153-
context.attributes = {};
150+
// eslint-disable-next-line no-unused-vars,class-methods-use-this
151+
async checkAuth(request, context) {
152+
if (context.env?.SKIP_AUTH !== 'true') {
153+
return null;
154154
}
155-
// Create a mock admin authInfo
156-
context.attributes.authInfo = new AuthInfo()
155+
// Defense-in-depth: refuse to skip auth in a deployed Lambda environment
156+
if (context.func?.name || process.env.AWS_LAMBDA_FUNCTION_NAME) {
157+
this.log('SKIP_AUTH is true but running in Lambda - ignoring', 'warn');
158+
return null;
159+
}
160+
this.log('SKIP_AUTH is true - injecting mock admin identity', 'info');
161+
return new AuthInfo()
157162
.withAuthenticated(true)
158163
.withProfile({
159164
user_id: 'local-dev-admin',
160165
email: 'admin@localhost',
161166
is_admin: true,
162-
// Empty tenants means hasOrganization will return false, but is_admin bypasses that
163167
tenants: [],
164168
})
165169
.withType('api_key')
166170
.withScopes([{ name: 'admin' }]);
167171
}
168-
/* c8 ignore stop */
172+
}
173+
/* c8 ignore stop */
174+
175+
/**
176+
* This is the main function
177+
* @param {Request} request the request object (see fetch api)
178+
* @param {UniversalContext} context the context of the universal serverless function
179+
* @returns {Response} a response
180+
*/
181+
async function run(request, context) {
182+
const { log, pathInfo } = context;
183+
const { route, suffix, method } = pathInfo;
169184

170185
if (!hasText(route)) {
171186
log.info(`Unable to extract path info. Wrong format: ${suffix}`);
@@ -329,7 +344,9 @@ const { WORKSPACE_EXTERNAL } = SLACK_TARGETS;
329344
// 2. authWrapper — handles JWT, IMS, scoped API key, legacy API key
330345
const wrappedMain = wrap(run)
331346
.with(authWrapper, {
332-
authHandlers: [JwtHandler, AdobeImsHandler, ScopedApiKeyHandler, LegacyApiKeyHandler],
347+
authHandlers: [
348+
SkipAuthHandler, JwtHandler, AdobeImsHandler, ScopedApiKeyHandler, LegacyApiKeyHandler,
349+
],
333350
})
334351
.with(s2sAuthWrapper, { routeCapabilities: routeRequiredCapabilities });
335352

0 commit comments

Comments
 (0)