Merge pull request #4542 from erikxiv/techdocs-auth

add authorization to techdocs api requests
This commit is contained in:
Patrik Oldsberg
2021-03-01 14:00:47 +01:00
committed by GitHub
8 changed files with 117 additions and 13 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-backend': patch
---
Forward authorization header on backend request if present
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': minor
---
Add authorization header on techdocs api requests. Breaking change as clients now needs the Identity API.
@@ -11,11 +11,35 @@ Caveat: as of writing this, Backstage does not refresh the identity token so eve
```typescript
// packages/backend/src/index.ts from a create-app deployment
import cookieParser from 'cookie-parser';
import { Request, Response, NextFunction } from 'express';
import { JWT } from 'jose';
import { URL } from 'url';
import { IdentityClient } from '@backstage/plugin-auth-backend';
// ...
function setTokenCookie(
res: Response,
options: { token: string; secure: boolean; cookieDomain: string },
) {
try {
const payload = JWT.decode(options.token) as object & {
exp: number;
};
res.cookie(`token`, options.token, {
expires: new Date(payload?.exp ? payload?.exp * 1000 : 0),
secure: options.secure,
sameSite: 'lax',
domain: options.cookieDomain,
path: '/',
httpOnly: true,
});
} catch (_err) {
// Ignore
}
}
async function main() {
// ...
@@ -24,14 +48,30 @@ async function main() {
discovery,
issuer: await discovery.getExternalBaseUrl('auth'),
});
const baseUrl = config.getString('backend.baseUrl');
const secure = baseUrl.startsWith('https://');
const cookieDomain = new URL(baseUrl).hostname;
const authMiddleware = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
const token = IdentityClient.getBearerToken(req.headers.authorization);
const token =
IdentityClient.getBearerToken(req.headers.authorization) ||
req.cookies['token'];
req.user = await identity.authenticate(token);
if (!req.headers.authorization) {
// Authorization header may be forwarded by plugin requests
req.headers.authorization = `Bearer ${token}`;
}
if (token !== req.cookies['token']) {
setTokenCookie(res, {
token,
secure,
cookieDomain,
});
}
next();
} catch (error) {
res.status(401).send(`Unauthorized`);
@@ -39,6 +79,7 @@ async function main() {
};
const apiRouter = Router();
apiRouter.use(cookieParser());
// The auth route must be publically available as it is used during login
apiRouter.use('/auth', await auth(authEnv));
// Only authenticated requests are allowed to the routes below
+12 -1
View File
@@ -82,9 +82,13 @@ export async function createRouter({
const { kind, namespace, name } = req.params;
try {
const token = getBearerToken(req.headers.authorization);
const entity = (await (
await fetch(
`${catalogUrl}/entities/by-name/${kind}/${namespace}/${name}`,
{
headers: token ? { Authorization: `Bearer ${token}` } : {},
},
)
).json()) as Entity;
@@ -109,7 +113,10 @@ export async function createRouter({
const catalogUrl = await discovery.getBaseUrl('catalog');
const triple = [kind, namespace, name].map(encodeURIComponent).join('/');
const catalogRes = await fetch(`${catalogUrl}/entities/by-name/${triple}`);
const token = getBearerToken(req.headers.authorization);
const catalogRes = await fetch(`${catalogUrl}/entities/by-name/${triple}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (!catalogRes.ok) {
const catalogResText = await catalogRes.text();
res.status(catalogRes.status);
@@ -202,3 +209,7 @@ export async function createRouter({
return router;
}
function getBearerToken(header?: string): string | undefined {
return header?.match(/(?:Bearer)\s+(\S+)/i)?.[1];
}
+9 -1
View File
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DiscoveryApi } from '@backstage/core';
import { DiscoveryApi, IdentityApi } from '@backstage/core';
import { Config } from '@backstage/config';
import { EntityName } from '@backstage/catalog-model';
import { TechDocsStorage } from '../src/api';
@@ -21,16 +21,20 @@ import { TechDocsStorage } from '../src/api';
export class TechDocsDevStorageApi implements TechDocsStorage {
public configApi: Config;
public discoveryApi: DiscoveryApi;
public identityApi: IdentityApi;
constructor({
configApi,
discoveryApi,
identityApi,
}: {
configApi: Config;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.configApi = configApi;
this.discoveryApi = discoveryApi;
this.identityApi = identityApi;
}
async getApiOrigin() {
@@ -45,9 +49,13 @@ export class TechDocsDevStorageApi implements TechDocsStorage {
const apiOrigin = await this.getApiOrigin();
const url = `${apiOrigin}/${name}/${path}`;
const token = await this.identityApi.getIdToken();
const request = await fetch(
`${url.endsWith('/') ? url : `${url}/`}index.html`,
{
headers: token ? { Authorization: `Bearer ${token}` } : {},
},
);
if (request.status === 404) {
+8 -3
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { configApiRef, discoveryApiRef } from '@backstage/core';
import { configApiRef, discoveryApiRef, identityApiRef } from '@backstage/core';
import { createDevApp } from '@backstage/dev-utils';
import { techdocsPlugin } from '../src/plugin';
import { TechDocsDevStorageApi } from './api';
@@ -23,11 +23,16 @@ import { techdocsStorageApiRef } from '../src';
createDevApp()
.registerApi({
api: techdocsStorageApiRef,
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
factory: ({ configApi, discoveryApi }) =>
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new TechDocsDevStorageApi({
configApi,
discoveryApi,
identityApi,
}),
})
.registerPlugin(techdocsPlugin)
+21 -3
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createApiRef, DiscoveryApi } from '@backstage/core';
import { createApiRef, DiscoveryApi, IdentityApi } from '@backstage/core';
import { Config } from '@backstage/config';
import { EntityName } from '@backstage/catalog-model';
import { TechDocsMetadata } from './types';
@@ -51,16 +51,20 @@ export interface TechDocs {
export class TechDocsApi implements TechDocs {
public configApi: Config;
public discoveryApi: DiscoveryApi;
public identityApi: IdentityApi;
constructor({
configApi,
discoveryApi,
identityApi,
}: {
configApi: Config;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.configApi = configApi;
this.discoveryApi = discoveryApi;
this.identityApi = identityApi;
}
async getApiOrigin() {
@@ -84,8 +88,11 @@ export class TechDocsApi implements TechDocs {
const apiOrigin = await this.getApiOrigin();
const requestUrl = `${apiOrigin}/metadata/techdocs/${namespace}/${kind}/${name}`;
const token = await this.identityApi.getIdToken();
const request = await fetch(`${requestUrl}`);
const request = await fetch(`${requestUrl}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const res = await request.json();
return res;
@@ -104,8 +111,11 @@ export class TechDocsApi implements TechDocs {
const apiOrigin = await this.getApiOrigin();
const requestUrl = `${apiOrigin}/metadata/entity/${namespace}/${kind}/${name}`;
const token = await this.identityApi.getIdToken();
const request = await fetch(`${requestUrl}`);
const request = await fetch(`${requestUrl}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const res = await request.json();
return res;
@@ -120,16 +130,20 @@ export class TechDocsApi implements TechDocs {
export class TechDocsStorageApi implements TechDocsStorage {
public configApi: Config;
public discoveryApi: DiscoveryApi;
public identityApi: IdentityApi;
constructor({
configApi,
discoveryApi,
identityApi,
}: {
configApi: Config;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.configApi = configApi;
this.discoveryApi = discoveryApi;
this.identityApi = identityApi;
}
async getApiOrigin() {
@@ -152,9 +166,13 @@ export class TechDocsStorageApi implements TechDocsStorage {
const apiOrigin = await this.getApiOrigin();
const url = `${apiOrigin}/docs/${namespace}/${kind}/${name}/${path}`;
const token = await this.identityApi.getIdToken();
const request = await fetch(
`${url.endsWith('/') ? url : `${url}/`}index.html`,
{
headers: token ? { Authorization: `Bearer ${token}` } : {},
},
);
let errorMessage = '';
+15 -4
View File
@@ -35,6 +35,7 @@ import {
createApiFactory,
configApiRef,
discoveryApiRef,
identityApiRef,
createRoutableExtension,
} from '@backstage/core';
import {
@@ -64,20 +65,30 @@ export const techdocsPlugin = createPlugin({
apis: [
createApiFactory({
api: techdocsStorageApiRef,
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
factory: ({ configApi, discoveryApi }) =>
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new TechDocsStorageApi({
configApi,
discoveryApi,
identityApi,
}),
}),
createApiFactory({
api: techdocsApiRef,
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
factory: ({ configApi, discoveryApi }) =>
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new TechDocsApi({
configApi,
discoveryApi,
identityApi,
}),
}),
],