mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-06 01:15:28 +02:00
fix: Allow AuthType None to use valid API tokens (#6247)
Fixes ##5799 and #5785 When you do not provide a token we should resolve to the "default" environment to maintain backward compatibility. If you actually provide a token we should prefer that and even block the request if it is not valid. An interesting fact is that "default" environment is not available on a fresh installation of Unleash. This means that you need to provide a token to actually get access to toggle configurations. --------- Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
parent
e5fe4a7766
commit
4a81f0932f
@ -17,7 +17,7 @@ import IndexRouter from './routes';
|
|||||||
import requestLogger from './middleware/request-logger';
|
import requestLogger from './middleware/request-logger';
|
||||||
import demoAuthentication from './middleware/demo-authentication';
|
import demoAuthentication from './middleware/demo-authentication';
|
||||||
import ossAuthentication from './middleware/oss-authentication';
|
import ossAuthentication from './middleware/oss-authentication';
|
||||||
import noAuthentication from './middleware/no-authentication';
|
import noAuthentication, { noApiToken } from './middleware/no-authentication';
|
||||||
import secureHeaders from './middleware/secure-headers';
|
import secureHeaders from './middleware/secure-headers';
|
||||||
|
|
||||||
import { loadIndexHTML } from './util/load-index-html';
|
import { loadIndexHTML } from './util/load-index-html';
|
||||||
@ -41,6 +41,7 @@ export default async function getApp(
|
|||||||
const baseUriPath = config.server.baseUriPath || '';
|
const baseUriPath = config.server.baseUriPath || '';
|
||||||
const publicFolder = config.publicFolder || findPublicFolder();
|
const publicFolder = config.publicFolder || findPublicFolder();
|
||||||
const indexHTML = await loadIndexHTML(config, publicFolder);
|
const indexHTML = await loadIndexHTML(config, publicFolder);
|
||||||
|
const logger = config.getLogger('lib/app.ts');
|
||||||
|
|
||||||
app.set('trust proxy', true);
|
app.set('trust proxy', true);
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
@ -147,6 +148,11 @@ export default async function getApp(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case IAuthType.NONE: {
|
case IAuthType.NONE: {
|
||||||
|
logger.warn(
|
||||||
|
'The AuthType=none option for Unleash is no longer recommended and will be removed in version 6.',
|
||||||
|
);
|
||||||
|
noApiToken(baseUriPath, app);
|
||||||
|
app.use(baseUriPath, apiTokenMiddleware(config, services));
|
||||||
noAuthentication(baseUriPath, app);
|
noAuthentication(baseUriPath, app);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,51 @@
|
|||||||
import { Application } from 'express';
|
import { Application } from 'express';
|
||||||
import NoAuthUser from '../types/no-auth-user';
|
import NoAuthUser from '../types/no-auth-user';
|
||||||
|
import { ApiTokenType } from '../types/models/api-token';
|
||||||
|
import {
|
||||||
|
ApiUser,
|
||||||
|
IApiRequest,
|
||||||
|
IAuthRequest,
|
||||||
|
permissions,
|
||||||
|
} from '../server-impl';
|
||||||
|
import { DEFAULT_ENV } from '../util';
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
function noneAuthentication(basePath: string, app: Application): void {
|
function noneAuthentication(baseUriPath: string, app: Application): void {
|
||||||
app.use(`${basePath || ''}/api/admin/`, (req, res, next) => {
|
app.use(
|
||||||
// @ts-expect-error
|
`${baseUriPath || ''}/api/admin/`,
|
||||||
if (!req.user) {
|
(req: IAuthRequest, res, next) => {
|
||||||
// @ts-expect-error
|
if (!req.user) {
|
||||||
req.user = new NoAuthUser();
|
req.user = new NoAuthUser();
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function noApiToken(baseUriPath: string, app: Application) {
|
||||||
|
app.use(`${baseUriPath}/api/frontend`, (req: IApiRequest, res, next) => {
|
||||||
|
if (!req.headers.authorization && !req.user) {
|
||||||
|
req.user = new ApiUser({
|
||||||
|
tokenName: 'unknown',
|
||||||
|
permissions: [permissions.FRONTEND],
|
||||||
|
projects: ['*'],
|
||||||
|
environment: DEFAULT_ENV,
|
||||||
|
type: ApiTokenType.FRONTEND,
|
||||||
|
secret: 'unknown',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
app.use(`${baseUriPath}/api/client`, (req: IApiRequest, res, next) => {
|
||||||
|
if (!req.headers.authorization && !req.user) {
|
||||||
|
req.user = new ApiUser({
|
||||||
|
tokenName: 'unknown',
|
||||||
|
permissions: [permissions.CLIENT],
|
||||||
|
projects: ['*'],
|
||||||
|
environment: DEFAULT_ENV,
|
||||||
|
type: ApiTokenType.CLIENT,
|
||||||
|
secret: 'unknown',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
@ -11,7 +11,6 @@ export const userSchema = {
|
|||||||
id: {
|
id: {
|
||||||
description: 'The user id',
|
description: 'The user id',
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
minimum: 0,
|
|
||||||
example: 123,
|
example: 123,
|
||||||
},
|
},
|
||||||
isAPI: {
|
isAPI: {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ADMIN } from './permissions';
|
import { ADMIN } from './permissions';
|
||||||
export default class NoAuthUser {
|
import { IUser } from './user';
|
||||||
|
export default class NoAuthUser implements IUser {
|
||||||
isAPI: boolean;
|
isAPI: boolean;
|
||||||
|
|
||||||
username: string;
|
username: string;
|
||||||
@ -8,6 +9,15 @@ export default class NoAuthUser {
|
|||||||
|
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
inviteLink?: string | undefined;
|
||||||
|
seenAt?: Date | undefined;
|
||||||
|
createdAt?: Date | undefined;
|
||||||
|
loginAttempts?: number | undefined;
|
||||||
|
imageUrl: string;
|
||||||
|
accountType?: 'User' | 'Service Account' | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
username: string = 'unknown',
|
username: string = 'unknown',
|
||||||
id: number = -1,
|
id: number = -1,
|
||||||
@ -15,6 +25,7 @@ export default class NoAuthUser {
|
|||||||
) {
|
) {
|
||||||
this.isAPI = true;
|
this.isAPI = true;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
|
this.name = 'unknown';
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.permissions = permissions;
|
this.permissions = permissions;
|
||||||
}
|
}
|
||||||
|
135
src/test/e2e/api/client/feature.auth-none.e2e.test.ts
Normal file
135
src/test/e2e/api/client/feature.auth-none.e2e.test.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import {
|
||||||
|
IUnleashTest,
|
||||||
|
setupAppWithCustomConfig,
|
||||||
|
} from '../../helpers/test-helper';
|
||||||
|
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||||
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
|
import { DEFAULT_ENV } from '../../../../lib/util/constants';
|
||||||
|
import User from '../../../../lib/types/user';
|
||||||
|
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||||
|
|
||||||
|
let app: IUnleashTest;
|
||||||
|
let db: ITestDb;
|
||||||
|
const testUser = { name: 'test', id: -9999 } as User;
|
||||||
|
let clientSecret: string;
|
||||||
|
let frontendSecret: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('feature_api_client_auth_none', getLogger);
|
||||||
|
app = await setupAppWithCustomConfig(
|
||||||
|
db.stores,
|
||||||
|
{
|
||||||
|
authentication: {
|
||||||
|
type: 'none',
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
strictSchemaValidation: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db.rawDatabase,
|
||||||
|
);
|
||||||
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
|
'default',
|
||||||
|
{
|
||||||
|
name: 'feature_1',
|
||||||
|
description: 'the #1 feature',
|
||||||
|
impressionData: true,
|
||||||
|
},
|
||||||
|
'test',
|
||||||
|
testUser.id,
|
||||||
|
);
|
||||||
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
|
'default',
|
||||||
|
{
|
||||||
|
name: 'feature_2',
|
||||||
|
description: 'soon to be the #1 feature',
|
||||||
|
},
|
||||||
|
'test',
|
||||||
|
testUser.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
|
'default',
|
||||||
|
{
|
||||||
|
name: 'feature_3',
|
||||||
|
description: 'terrible feature',
|
||||||
|
},
|
||||||
|
'test',
|
||||||
|
testUser.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = await app.services.apiTokenService.createApiTokenWithProjects(
|
||||||
|
{
|
||||||
|
tokenName: 'test',
|
||||||
|
type: ApiTokenType.CLIENT,
|
||||||
|
environment: DEFAULT_ENV,
|
||||||
|
projects: ['default'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
clientSecret = token.secret;
|
||||||
|
|
||||||
|
const frontendToken =
|
||||||
|
await app.services.apiTokenService.createApiTokenWithProjects({
|
||||||
|
tokenName: 'test',
|
||||||
|
type: ApiTokenType.FRONTEND,
|
||||||
|
environment: DEFAULT_ENV,
|
||||||
|
projects: ['default'],
|
||||||
|
});
|
||||||
|
frontendSecret = frontendToken.secret;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns three feature toggles', async () => {
|
||||||
|
return app.request
|
||||||
|
.get('/api/client/features')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.features).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 401 for incorrect api token', async () => {
|
||||||
|
return app.request
|
||||||
|
.get('/api/client/features')
|
||||||
|
.set('Authorization', 'some-invalid-token')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns success for correct api token', async () => {
|
||||||
|
return app.request
|
||||||
|
.get('/api/client/features')
|
||||||
|
.set('Authorization', clientSecret)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns successful for frontend API without token', async () => {
|
||||||
|
return app.request
|
||||||
|
.get('/api/frontend')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 401 for frontend API with invalid token', async () => {
|
||||||
|
return app.request
|
||||||
|
.get('/api/frontend')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.set('Authorization', 'some-invalid-token')
|
||||||
|
.expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 200 for frontend API with valid token', async () => {
|
||||||
|
return app.request
|
||||||
|
.get('/api/frontend')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.set('Authorization', frontendSecret)
|
||||||
|
.expect(200);
|
||||||
|
});
|
@ -21,7 +21,7 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
'default',
|
'default',
|
||||||
{
|
{
|
||||||
name: 'featureX',
|
name: 'featureX',
|
||||||
@ -31,7 +31,7 @@ beforeAll(async () => {
|
|||||||
'test',
|
'test',
|
||||||
testUser.id,
|
testUser.id,
|
||||||
);
|
);
|
||||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
'default',
|
'default',
|
||||||
{
|
{
|
||||||
name: 'featureY',
|
name: 'featureY',
|
||||||
@ -40,7 +40,7 @@ beforeAll(async () => {
|
|||||||
'test',
|
'test',
|
||||||
testUser.id,
|
testUser.id,
|
||||||
);
|
);
|
||||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
'default',
|
'default',
|
||||||
{
|
{
|
||||||
name: 'featureZ',
|
name: 'featureZ',
|
||||||
@ -49,7 +49,7 @@ beforeAll(async () => {
|
|||||||
'test',
|
'test',
|
||||||
testUser.id,
|
testUser.id,
|
||||||
);
|
);
|
||||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
'default',
|
'default',
|
||||||
{
|
{
|
||||||
name: 'featureArchivedX',
|
name: 'featureArchivedX',
|
||||||
@ -59,12 +59,12 @@ beforeAll(async () => {
|
|||||||
testUser.id,
|
testUser.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.services.featureToggleServiceV2.archiveToggle(
|
await app.services.featureToggleService.archiveToggle(
|
||||||
'featureArchivedX',
|
'featureArchivedX',
|
||||||
testUser,
|
testUser,
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
'default',
|
'default',
|
||||||
{
|
{
|
||||||
name: 'featureArchivedY',
|
name: 'featureArchivedY',
|
||||||
@ -74,11 +74,11 @@ beforeAll(async () => {
|
|||||||
testUser.id,
|
testUser.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.services.featureToggleServiceV2.archiveToggle(
|
await app.services.featureToggleService.archiveToggle(
|
||||||
'featureArchivedY',
|
'featureArchivedY',
|
||||||
testUser,
|
testUser,
|
||||||
);
|
);
|
||||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
'default',
|
'default',
|
||||||
{
|
{
|
||||||
name: 'featureArchivedZ',
|
name: 'featureArchivedZ',
|
||||||
@ -87,11 +87,11 @@ beforeAll(async () => {
|
|||||||
'test',
|
'test',
|
||||||
testUser.id,
|
testUser.id,
|
||||||
);
|
);
|
||||||
await app.services.featureToggleServiceV2.archiveToggle(
|
await app.services.featureToggleService.archiveToggle(
|
||||||
'featureArchivedZ',
|
'featureArchivedZ',
|
||||||
testUser,
|
testUser,
|
||||||
);
|
);
|
||||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
'default',
|
'default',
|
||||||
{
|
{
|
||||||
name: 'feature.with.variants',
|
name: 'feature.with.variants',
|
||||||
@ -100,7 +100,7 @@ beforeAll(async () => {
|
|||||||
'test',
|
'test',
|
||||||
testUser.id,
|
testUser.id,
|
||||||
);
|
);
|
||||||
await app.services.featureToggleServiceV2.saveVariants(
|
await app.services.featureToggleService.saveVariants(
|
||||||
'feature.with.variants',
|
'feature.with.variants',
|
||||||
'default',
|
'default',
|
||||||
[
|
[
|
||||||
@ -132,19 +132,19 @@ test('returns calculated hash', async () => {
|
|||||||
.get('/api/client/features')
|
.get('/api/client/features')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
expect(res.headers.etag).toBe('"ae443048:16"');
|
expect(res.headers.etag).toBe('"61824cd0:16"');
|
||||||
expect(res.body.meta.etag).toBe('"ae443048:16"');
|
expect(res.body.meta.etag).toBe('"61824cd0:16"');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns 304 for pre-calculated hash', async () => {
|
test('returns 304 for pre-calculated hash', async () => {
|
||||||
return app.request
|
return app.request
|
||||||
.get('/api/client/features')
|
.get('/api/client/features')
|
||||||
.set('if-none-match', '"ae443048:16"')
|
.set('if-none-match', '"61824cd0:16"')
|
||||||
.expect(304);
|
.expect(304);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns 200 when content updates and hash does not match anymore', async () => {
|
test('returns 200 when content updates and hash does not match anymore', async () => {
|
||||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
'default',
|
'default',
|
||||||
{
|
{
|
||||||
name: 'featureNew304',
|
name: 'featureNew304',
|
||||||
@ -160,6 +160,6 @@ test('returns 200 when content updates and hash does not match anymore', async (
|
|||||||
.set('if-none-match', 'ae443048:16')
|
.set('if-none-match', 'ae443048:16')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(res.headers.etag).toBe('"ae443048:17"');
|
expect(res.headers.etag).toBe('"61824cd0:17"');
|
||||||
expect(res.body.meta.etag).toBe('"ae443048:17"');
|
expect(res.body.meta.etag).toBe('"61824cd0:17"');
|
||||||
});
|
});
|
||||||
|
@ -63,8 +63,8 @@ unleash.start(unleashOptions);
|
|||||||
- `type` / `AUTH_TYPE`: `string` — What kind of authentication to use. Possible values
|
- `type` / `AUTH_TYPE`: `string` — What kind of authentication to use. Possible values
|
||||||
- `open-source` - Sign in with username and password. This is the default value.
|
- `open-source` - Sign in with username and password. This is the default value.
|
||||||
- `custom` - If implementing your own authentication hook, use this
|
- `custom` - If implementing your own authentication hook, use this
|
||||||
- `none` - Turn off authentication all together
|
|
||||||
- `demo` - Only requires an email to sign in (was default in v3)
|
- `demo` - Only requires an email to sign in (was default in v3)
|
||||||
|
- `none` - _Deprecated_ Turn off authentication completely. If no API token is provided towards /`api/client` or `/api/frontend` you will receive configuration for the "default" environment. We generally recommend you use the `demo` type for simple, insecure usage of Unleash. This auth type has many known limitations, particularly related to personalized capabilities such as favorites and [notifications](../../reference/notifications.md).
|
||||||
- `customAuthHandler`: function `(app: any, config: IUnleashConfig): void` — custom express middleware handling authentication. Used when type is set to `custom`. Can not be set via environment variables.
|
- `customAuthHandler`: function `(app: any, config: IUnleashConfig): void` — custom express middleware handling authentication. Used when type is set to `custom`. Can not be set via environment variables.
|
||||||
- `initialAdminUser`: `{ username: string, password: string} | null` — whether to create an admin user with default password - Defaults to using `admin` and `unleash4all` as the username and password. Can not be overridden by setting the `UNLEASH_DEFAULT_ADMIN_USERNAME` and `UNLEASH_DEFAULT_ADMIN_PASSWORD` environment variables.
|
- `initialAdminUser`: `{ username: string, password: string} | null` — whether to create an admin user with default password - Defaults to using `admin` and `unleash4all` as the username and password. Can not be overridden by setting the `UNLEASH_DEFAULT_ADMIN_USERNAME` and `UNLEASH_DEFAULT_ADMIN_PASSWORD` environment variables.
|
||||||
- `initApiTokens` / `INIT_ADMIN_API_TOKENS`, `INIT_CLIENT_API_TOKENS`, and `INIT_FRONTEND_API_TOKENS`: `ApiTokens[]` — Array of API tokens to create on startup. The tokens will only be created if the database doesn't already contain any API tokens. Example:
|
- `initApiTokens` / `INIT_ADMIN_API_TOKENS`, `INIT_CLIENT_API_TOKENS`, and `INIT_FRONTEND_API_TOKENS`: `ApiTokens[]` — Array of API tokens to create on startup. The tokens will only be created if the database doesn't already contain any API tokens. Example:
|
||||||
|
Loading…
Reference in New Issue
Block a user