From 04c3395f41f6458870489778bba1a793423aeced Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 23 Mar 2021 00:17:39 +0100 Subject: [PATCH 1/4] feat: document method to set token cookie Signed-off-by: Erik Larsson --- .../tutorials/authenticate-api-requests.md | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index 4f4547a49e..bf04d9cdef 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -6,7 +6,7 @@ API requests from frontend plugins include an authorization header with a Backst Note that this means Backstage will stop working for guests, as no token is issued for them. -Caveat: as of writing this, Backstage does not refresh the identity token so eventually users will get a 401 response on API calls (not on loading the web page as only the API calls are authenticated) and have to logout/login again to get a new token. +As techdocs HTML pages load assets without an Authorization header the code below also sets a token cookie when the user logs in (and when the token is about to expire). ```typescript // packages/backend/src/index.ts from a create-app deployment @@ -82,6 +82,10 @@ async function main() { apiRouter.use(cookieParser()); // The auth route must be publically available as it is used during login apiRouter.use('/auth', await auth(authEnv)); + // Add a simple endpoint to be used when setting a token cookie + apiRouter.use('/cookie', authMiddleware, (req, res) => { + res.status(200).send(`Coming right up`); + }); // Only authenticated requests are allowed to the routes below apiRouter.use('/catalog', authMiddleware, await catalog(catalogEnv)); apiRouter.use('/techdocs', authMiddleware, await techdocs(techdocsEnv)); @@ -91,3 +95,85 @@ async function main() { // ... } ``` + +```typescript +// packages/app/src/App.tsx from a create-app deployment + +import { + discoveryApiRef, + useApi, +} from '@backstage/core'; + +// ... + +// Parses supplied JWT token and returns the payload +function parseJwt(token) { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join(''), + ); + + return JSON.parse(jsonPayload); +} + +// Returns milliseconds until the supplied JWT token expires +function msUntilExpiry(token) { + const payload = parseJwt(token); + const remaining = new Date(payload.exp * 1000) - new Date(); + return remaining; +} + +// Calls the specified url regularly using an auth token to set a token cookie +// to authorize regular HTTP requests when loading techdocs +async function setAuthCookie(url, getIdToken) { + const token = await getIdToken(); + const response = await fetch(url, { + mode: 'cors', + credentials: 'include', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + // Call this function again a few minutes before the token expires + const ms = msUntilExpiry(token) - 4 * 60 * 1000; + setTimeout( + () => { + setAuthCookie(url, getIdToken); + }, + ms > 0 ? ms : 10000, + ); +} + +const app = createApp({ + + // ... + + components: { + SignInPage: props => { + const discoveryApi = useApi(discoveryApiRef); + return ( + { + // When logged in, set a token cookie + setAuthCookie( + await discoveryApi.getBaseUrl('cookie'), + result.getIdToken, + ); + // Forward results + props.onResult(result); + }} + /> + ); + }, + }, +``` From 6f40c8b66b5eac73e5d3c91b6e715bb9cd32080a Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 23 Mar 2021 00:21:30 +0100 Subject: [PATCH 2/4] feat: document method to set token cookie Signed-off-by: Erik Larsson --- contrib/docs/tutorials/authenticate-api-requests.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index bf04d9cdef..897e2378e7 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -99,10 +99,7 @@ async function main() { ```typescript // packages/app/src/App.tsx from a create-app deployment -import { - discoveryApiRef, - useApi, -} from '@backstage/core'; +import { discoveryApiRef, useApi } from '@backstage/core'; // ... @@ -151,7 +148,6 @@ async function setAuthCookie(url, getIdToken) { } const app = createApp({ - // ... components: { @@ -176,4 +172,9 @@ const app = createApp({ ); }, }, + + // ... +}); + +// ... ``` From 7e0ca4eec33e2f5689ee4aaa59aa1482cb5812bc Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 23 Mar 2021 00:26:00 +0100 Subject: [PATCH 3/4] feat: document method to set token cookie Signed-off-by: Erik Larsson --- contrib/docs/tutorials/authenticate-api-requests.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index 897e2378e7..c0ab7f9ceb 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -128,7 +128,7 @@ function msUntilExpiry(token) { // Calls the specified url regularly using an auth token to set a token cookie // to authorize regular HTTP requests when loading techdocs -async function setAuthCookie(url, getIdToken) { +async function setTokenCookie(url, getIdToken) { const token = await getIdToken(); const response = await fetch(url, { mode: 'cors', @@ -141,7 +141,7 @@ async function setAuthCookie(url, getIdToken) { const ms = msUntilExpiry(token) - 4 * 60 * 1000; setTimeout( () => { - setAuthCookie(url, getIdToken); + setTokenCookie(url, getIdToken); }, ms > 0 ? ms : 10000, ); @@ -161,7 +161,7 @@ const app = createApp({ align="center" onResult={async result => { // When logged in, set a token cookie - setAuthCookie( + setTokenCookie( await discoveryApi.getBaseUrl('cookie'), result.getIdToken, ); From 0676e320491d8f183f5d2de3d244dd849d44afbf Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Tue, 23 Mar 2021 19:49:50 +0100 Subject: [PATCH 4/4] Typescript Signed-off-by: Erik Larsson --- .../tutorials/authenticate-api-requests.md | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index c0ab7f9ceb..7422fa03ec 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -83,7 +83,7 @@ async function main() { // The auth route must be publically available as it is used during login apiRouter.use('/auth', await auth(authEnv)); // Add a simple endpoint to be used when setting a token cookie - apiRouter.use('/cookie', authMiddleware, (req, res) => { + apiRouter.use('/cookie', authMiddleware, (_req, res) => { res.status(200).send(`Coming right up`); }); // Only authenticated requests are allowed to the routes below @@ -104,7 +104,7 @@ import { discoveryApiRef, useApi } from '@backstage/core'; // ... // Parses supplied JWT token and returns the payload -function parseJwt(token) { +function parseJwt(token: string): { exp: number } { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent( @@ -120,17 +120,18 @@ function parseJwt(token) { } // Returns milliseconds until the supplied JWT token expires -function msUntilExpiry(token) { +function msUntilExpiry(token: string): number { const payload = parseJwt(token); - const remaining = new Date(payload.exp * 1000) - new Date(); + const remaining = + new Date(payload.exp * 1000).getTime() - new Date().getTime(); return remaining; } // Calls the specified url regularly using an auth token to set a token cookie // to authorize regular HTTP requests when loading techdocs -async function setTokenCookie(url, getIdToken) { +async function setTokenCookie(url: string, getIdToken: () => Promise) { const token = await getIdToken(); - const response = await fetch(url, { + await fetch(url, { mode: 'cors', credentials: 'include', headers: { @@ -161,10 +162,12 @@ const app = createApp({ align="center" onResult={async result => { // When logged in, set a token cookie - setTokenCookie( - await discoveryApi.getBaseUrl('cookie'), - result.getIdToken, - ); + if (typeof result.getIdToken !== 'undefined') { + setTokenCookie( + await discoveryApi.getBaseUrl('cookie'), + result.getIdToken, + ); + } // Forward results props.onResult(result); }}