mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01: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 demoAuthentication from './middleware/demo-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 { loadIndexHTML } from './util/load-index-html';
|
||||
@ -41,6 +41,7 @@ export default async function getApp(
|
||||
const baseUriPath = config.server.baseUriPath || '';
|
||||
const publicFolder = config.publicFolder || findPublicFolder();
|
||||
const indexHTML = await loadIndexHTML(config, publicFolder);
|
||||
const logger = config.getLogger('lib/app.ts');
|
||||
|
||||
app.set('trust proxy', true);
|
||||
app.disable('x-powered-by');
|
||||
@ -147,6 +148,11 @@ export default async function getApp(
|
||||
break;
|
||||
}
|
||||
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);
|
||||
break;
|
||||
}
|
||||
|
@ -1,15 +1,53 @@
|
||||
import { Application } from 'express';
|
||||
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
|
||||
function noneAuthentication(basePath: string, app: Application): void {
|
||||
app.use(`${basePath || ''}/api/admin/`, (req, res, next) => {
|
||||
// @ts-expect-error
|
||||
function noneAuthentication(baseUriPath: string, app: Application): void {
|
||||
app.use(
|
||||
`${baseUriPath || ''}/api/admin/`,
|
||||
(req: IAuthRequest, res, next) => {
|
||||
if (!req.user) {
|
||||
// @ts-expect-error
|
||||
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();
|
||||
});
|
||||
}
|
||||
export default noneAuthentication;
|
||||
|
@ -11,7 +11,6 @@ export const userSchema = {
|
||||
id: {
|
||||
description: 'The user id',
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
example: 123,
|
||||
},
|
||||
isAPI: {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ADMIN } from './permissions';
|
||||
export default class NoAuthUser {
|
||||
import { IUser } from './user';
|
||||
export default class NoAuthUser implements IUser {
|
||||
isAPI: boolean;
|
||||
|
||||
username: string;
|
||||
@ -8,6 +9,15 @@ export default class NoAuthUser {
|
||||
|
||||
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(
|
||||
username: string = 'unknown',
|
||||
id: number = -1,
|
||||
@ -15,6 +25,7 @@ export default class NoAuthUser {
|
||||
) {
|
||||
this.isAPI = true;
|
||||
this.username = username;
|
||||
this.name = 'unknown';
|
||||
this.id = id;
|
||||
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',
|
||||
{
|
||||
name: 'featureX',
|
||||
@ -31,7 +31,7 @@ beforeAll(async () => {
|
||||
'test',
|
||||
testUser.id,
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
await app.services.featureToggleService.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
name: 'featureY',
|
||||
@ -40,7 +40,7 @@ beforeAll(async () => {
|
||||
'test',
|
||||
testUser.id,
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
await app.services.featureToggleService.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
name: 'featureZ',
|
||||
@ -49,7 +49,7 @@ beforeAll(async () => {
|
||||
'test',
|
||||
testUser.id,
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
await app.services.featureToggleService.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
name: 'featureArchivedX',
|
||||
@ -59,12 +59,12 @@ beforeAll(async () => {
|
||||
testUser.id,
|
||||
);
|
||||
|
||||
await app.services.featureToggleServiceV2.archiveToggle(
|
||||
await app.services.featureToggleService.archiveToggle(
|
||||
'featureArchivedX',
|
||||
testUser,
|
||||
);
|
||||
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
await app.services.featureToggleService.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
name: 'featureArchivedY',
|
||||
@ -74,11 +74,11 @@ beforeAll(async () => {
|
||||
testUser.id,
|
||||
);
|
||||
|
||||
await app.services.featureToggleServiceV2.archiveToggle(
|
||||
await app.services.featureToggleService.archiveToggle(
|
||||
'featureArchivedY',
|
||||
testUser,
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
await app.services.featureToggleService.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
name: 'featureArchivedZ',
|
||||
@ -87,11 +87,11 @@ beforeAll(async () => {
|
||||
'test',
|
||||
testUser.id,
|
||||
);
|
||||
await app.services.featureToggleServiceV2.archiveToggle(
|
||||
await app.services.featureToggleService.archiveToggle(
|
||||
'featureArchivedZ',
|
||||
testUser,
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
await app.services.featureToggleService.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
name: 'feature.with.variants',
|
||||
@ -100,7 +100,7 @@ beforeAll(async () => {
|
||||
'test',
|
||||
testUser.id,
|
||||
);
|
||||
await app.services.featureToggleServiceV2.saveVariants(
|
||||
await app.services.featureToggleService.saveVariants(
|
||||
'feature.with.variants',
|
||||
'default',
|
||||
[
|
||||
@ -132,19 +132,19 @@ test('returns calculated hash', async () => {
|
||||
.get('/api/client/features')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
expect(res.headers.etag).toBe('"ae443048:16"');
|
||||
expect(res.body.meta.etag).toBe('"ae443048:16"');
|
||||
expect(res.headers.etag).toBe('"61824cd0:16"');
|
||||
expect(res.body.meta.etag).toBe('"61824cd0:16"');
|
||||
});
|
||||
|
||||
test('returns 304 for pre-calculated hash', async () => {
|
||||
return app.request
|
||||
.get('/api/client/features')
|
||||
.set('if-none-match', '"ae443048:16"')
|
||||
.set('if-none-match', '"61824cd0:16"')
|
||||
.expect(304);
|
||||
});
|
||||
|
||||
test('returns 200 when content updates and hash does not match anymore', async () => {
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
await app.services.featureToggleService.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
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')
|
||||
.expect(200);
|
||||
|
||||
expect(res.headers.etag).toBe('"ae443048:17"');
|
||||
expect(res.body.meta.etag).toBe('"ae443048:17"');
|
||||
expect(res.headers.etag).toBe('"61824cd0: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
|
||||
- `open-source` - Sign in with username and password. This is the default value.
|
||||
- `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)
|
||||
- `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.
|
||||
- `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:
|
||||
|
Loading…
Reference in New Issue
Block a user