From f18b51d9326369d734362b406003f0cb4717e98a Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Thu, 18 Feb 2021 03:35:45 +0100 Subject: [PATCH] set and use access-token cookie --- .../tutorials/authenticate-api-requests.md | 6 +++- .../src/lib/oauth/OAuthAdapter.ts | 33 +++++++++++++++++++ .../techdocs-backend/src/service/router.ts | 16 ++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index 2b8407abd2..4698293152 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -11,6 +11,7 @@ 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 { IdentityClient } from '@backstage/plugin-auth-backend'; @@ -30,7 +31,9 @@ async function main() { next: NextFunction, ) => { try { - const token = IdentityClient.getBearerToken(req.headers.authorization); + const token = + IdentityClient.getBearerToken(req.headers.authorization) || + req.cookies['access-token']; req.user = await identity.authenticate(token); next(); } catch (error) { @@ -39,6 +42,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 diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 62cb8e444d..2da831950a 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -16,6 +16,7 @@ import express from 'express'; import crypto from 'crypto'; +import { JWT } from 'jose'; import { URL } from 'url'; import { AuthProviderRouteHandlers, @@ -127,6 +128,9 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { } await this.populateIdentity(response.backstageIdentity); + if (response.backstageIdentity?.idToken) { + this.setAccessTokenCookie(res, response.backstageIdentity?.idToken); + } // post message back to popup if successful return postMessageResponse(res, this.options.appOrigin, { @@ -151,6 +155,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { return; } + this.removeAccessTokenCookie(res); if (!this.options.disableRefresh) { // remove refresh token cookie before logout this.removeRefreshTokenCookie(res); @@ -190,6 +195,9 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { ); await this.populateIdentity(response.backstageIdentity); + if (response.backstageIdentity?.idToken) { + this.setAccessTokenCookie(res, response.backstageIdentity?.idToken); + } if ( response.providerInfo.refreshToken && @@ -246,6 +254,20 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { return req.cookies[`${providerId}-scope`]; }; + private setAccessTokenCookie = ( + res: express.Response, + accessToken: string, + ) => { + res.cookie(`access-token`, accessToken, { + expires: new Date(JWT.decode(accessToken).exp * 1000), + secure: this.options.secure, + sameSite: 'lax', + domain: this.options.cookieDomain, + path: '/api', + httpOnly: true, + }); + }; + private setRefreshTokenCookie = ( res: express.Response, refreshToken: string, @@ -260,6 +282,17 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { }); }; + private removeAccessTokenCookie = (res: express.Response) => { + res.cookie(`access-token`, '', { + maxAge: 0, + secure: this.options.secure, + sameSite: 'lax', + domain: this.options.cookieDomain, + path: '/api', + httpOnly: true, + }); + }; + private removeRefreshTokenCookie = (res: express.Response) => { res.cookie(`${this.options.providerId}-refresh-token`, '', { maxAge: 0, diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index ef032e9f8b..885a1a64af 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -82,9 +82,15 @@ export async function createRouter({ const { kind, namespace, name } = req.params; try { + const token = + getBearerToken(req.headers.authorization) || + req.cookies['access-token']; const entity = (await ( await fetch( `${catalogUrl}/entities/by-name/${kind}/${namespace}/${name}`, + { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }, ) ).json()) as Entity; @@ -109,7 +115,11 @@ 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) || req.cookies['access-token']; + 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 +212,7 @@ export async function createRouter({ return router; } + +function getBearerToken(header?: string): string | undefined { + return header?.match(/(?:Bearer|Basic)\s+(\S+)/i)?.[1]; +}