Merge pull request #23719 from backstage/rugvip/app-auth

[Auth] Migrate `app-backend` to use new auth services
This commit is contained in:
Patrik Oldsberg
2024-04-09 17:45:46 +02:00
committed by GitHub
46 changed files with 1239 additions and 118 deletions
@@ -181,6 +181,13 @@ class DefaultHttpAuthService implements HttpAuthService {
let credentials: BackstageCredentials<BackstageUserPrincipal>;
if (options?.credentials) {
if (this.#auth.isPrincipal(options.credentials, 'none')) {
res.clearCookie(
BACKSTAGE_AUTH_COOKIE,
await this.#getCookieOptions(res.req),
);
return { expiresAt: new Date() };
}
if (!this.#auth.isPrincipal(options.credentials, 'user')) {
throw new AuthenticationError(
'Refused to issue cookie for non-user principal',
@@ -196,16 +203,6 @@ class DefaultHttpAuthService implements HttpAuthService {
return { expiresAt: existingExpiresAt };
}
const originHeader = res.req.headers.origin;
const origin =
!originHeader || originHeader === 'null' ? undefined : originHeader;
// https://backstage.example.com/api/catalog
const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(
this.#pluginId,
);
const externalBaseUrl = new URL(origin ?? externalBaseUrlStr);
const { token, expiresAt } = await this.#auth.getLimitedUserToken(
credentials,
);
@@ -213,20 +210,41 @@ class DefaultHttpAuthService implements HttpAuthService {
throw new Error('User credentials is unexpectedly missing token');
}
res.cookie(BACKSTAGE_AUTH_COOKIE, token, {
...(await this.#getCookieOptions(res.req)),
expires: expiresAt,
});
return { expiresAt };
}
async #getCookieOptions(req: Request): Promise<{
domain: string;
httpOnly: true;
secure: boolean;
priority: 'high';
sameSite: 'none' | 'lax';
}> {
const originHeader = req.headers.origin;
const origin =
!originHeader || originHeader === 'null' ? undefined : originHeader;
const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(
this.#pluginId,
);
const externalBaseUrl = new URL(origin ?? externalBaseUrlStr);
const secure =
externalBaseUrl.protocol === 'https:' ||
externalBaseUrl.hostname === 'localhost';
res.cookie(BACKSTAGE_AUTH_COOKIE, token, {
return {
domain: externalBaseUrl.hostname,
httpOnly: true,
expires: expiresAt,
secure,
priority: 'high',
sameSite: secure ? 'none' : 'lax',
});
return { expiresAt };
};
}
async #existingCookieExpiration(req: Request): Promise<Date | undefined> {
@@ -0,0 +1,49 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import request from 'supertest';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
import { createCookieAuthRefreshMiddleware } from './createCookieAuthRefreshMiddleware';
describe('createCookieAuthRefreshMiddleware', () => {
let app: express.Express;
beforeAll(async () => {
const auth = mockServices.auth();
const httpAuth = mockServices.httpAuth();
const router = createCookieAuthRefreshMiddleware({ auth, httpAuth });
app = express().use(router);
});
beforeEach(() => {
jest.resetAllMocks();
});
it('should issue the user cookie', async () => {
const response = await request(app).get('/.backstage/auth/v1/cookie');
expect(response.status).toBe(200);
expect(response.header['set-cookie'][0]).toMatch(
`backstage-auth=${mockCredentials.limitedUser.token()}`,
);
});
it('should remove the user cookie', async () => {
const response = await request(app).delete('/.backstage/auth/v1/cookie');
expect(response.status).toBe(204);
expect(response.header['set-cookie'][0]).toMatch('backstage-auth=');
});
});
@@ -0,0 +1,47 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api';
import Router from 'express-promise-router';
const WELL_KNOWN_COOKIE_PATH_V1 = '/.backstage/auth/v1/cookie';
/**
* @public
* Creates a middleware that can be used to refresh the cookie for the user.
*/
export function createCookieAuthRefreshMiddleware(options: {
auth: AuthService;
httpAuth: HttpAuthService;
}) {
const { auth, httpAuth } = options;
const router = Router();
// Endpoint that sets the cookie for the user
router.get(WELL_KNOWN_COOKIE_PATH_V1, async (_, res) => {
const { expiresAt } = await httpAuth.issueUserCookie(res);
res.json({ expiresAt: expiresAt.toISOString() });
});
// Endpoint that removes the cookie for the user
router.delete(WELL_KNOWN_COOKIE_PATH_V1, async (_, res) => {
const credentials = await auth.getNoneCredentials();
await httpAuth.issueUserCookie(res, { credentials });
res.status(204).end();
});
return router;
}
@@ -14,16 +14,17 @@
* limitations under the License.
*/
import { Handler } from 'express';
import PromiseRouter from 'express-promise-router';
import {
coreServices,
createServiceFactory,
HttpRouterServiceAuthPolicy,
} from '@backstage/backend-plugin-api';
import { Handler } from 'express';
import PromiseRouter from 'express-promise-router';
import { createLifecycleMiddleware } from './createLifecycleMiddleware';
import { createCredentialsBarrier } from './createCredentialsBarrier';
import { createAuthIntegrationRouter } from './createAuthIntegrationRouter';
import { createCookieAuthRefreshMiddleware } from './createCookieAuthRefreshMiddleware';
/**
* @public
@@ -77,6 +78,7 @@ export const httpRouterServiceFactory = createServiceFactory(
router.use(createLifecycleMiddleware({ lifecycle }));
router.use(createAuthIntegrationRouter({ auth }));
router.use(credentialsBarrier.middleware);
router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth }));
return {
use(handler: Handler): void {