diff --git a/.changeset/spotty-kids-pay.md b/.changeset/spotty-kids-pay.md new file mode 100644 index 0000000000..157515201d --- /dev/null +++ b/.changeset/spotty-kids-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Ability for user to configure backstage token expiration diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index ce7525fb6b..290745454e 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -165,3 +165,14 @@ To try out SAML, you can use the mock identity provider: ## Links - [The Backstage homepage](https://backstage.io) + +## Configuring Token Expiration in App Config + +If you need to change Backstage token expiration from the default value of one hour you can do so through configuration. Note that this is **not** the session duration, but rather the duration that the short-term cryptographic tokens are valid for. The expiration can not be set lower than 10 minutes or above 24 hours. + +This is what the configuration looks like: + +``` +auth: + backstageTokenExpiration: { minutes: } +``` diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 9dbb89fada..25cd8d88ec 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { /** Configuration options for the auth plugin */ auth?: { @@ -184,6 +186,10 @@ export interface Config { cfaccess?: { teamName: string; }; + /** + * The backstage token expiration. + */ + backstageTokenExpiration?: HumanDuration; }; }; } diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index eb0d30a9ba..8a77059c0a 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -51,6 +51,7 @@ "@backstage/plugin-auth-backend-module-okta-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", + "@backstage/types": "workspace:^", "@google-cloud/firestore": "^7.0.0", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", diff --git a/plugins/auth-backend/src/service/router.test.ts b/plugins/auth-backend/src/service/router.test.ts index 3a1fde881e..8805a60e56 100644 --- a/plugins/auth-backend/src/service/router.test.ts +++ b/plugins/auth-backend/src/service/router.test.ts @@ -15,7 +15,11 @@ */ import { ConfigReader } from '@backstage/config'; -import { createOriginFilter } from './router'; +import { + createOriginFilter, + getDefaultBackstageTokenExpiryTime, +} from './router'; +import { BACKSTAGE_SESSION_EXPIRATION } from '../lib/session'; describe('Auth origin filtering', () => { const config = new ConfigReader({ @@ -52,3 +56,64 @@ describe('Auth origin filtering', () => { expect(createOriginFilter(config)(origin)).toBeTruthy(); }); }); + +describe('Test for default backstage token expiry time', () => { + it('Will return default backstage session expiration', () => { + const config = new ConfigReader({ + app: { + baseUrl: 'http://example.com/extra-path', + }, + }); + expect(getDefaultBackstageTokenExpiryTime(config)).toBe( + BACKSTAGE_SESSION_EXPIRATION, + ); + }); + + it('Will return user defined 120 minutes as backstage session expiration', () => { + const config = new ConfigReader({ + app: { + baseUrl: 'http://example.com/extra-path', + }, + auth: { + backstageTokenExpiration: { minutes: 120 }, + }, + }); + expect(getDefaultBackstageTokenExpiryTime(config)).toBe(7200); + }); + + it('Will return minimum duration of 10 minutes as backstage session expiration', () => { + const config = new ConfigReader({ + app: { + baseUrl: 'http://example.com/extra-path', + }, + auth: { + backstageTokenExpiration: { minutes: 2 }, + }, + }); + expect(getDefaultBackstageTokenExpiryTime(config)).toBe(600); + }); + + it('Will return user configured value as backstage session expiration', () => { + const config = new ConfigReader({ + app: { + baseUrl: 'http://example.com/extra-path', + }, + auth: { + backstageTokenExpiration: { minutes: 20 }, + }, + }); + expect(getDefaultBackstageTokenExpiryTime(config)).toBe(1200); + }); + + it('Will return maximum of 24 hour as backstage session expiration if user configured value is more than a day', () => { + const config = new ConfigReader({ + app: { + baseUrl: 'http://example.com/extra-path', + }, + auth: { + backstageTokenExpiration: { minutes: 1500 }, + }, + }); + expect(getDefaultBackstageTokenExpiryTime(config)).toBe(86400); + }); +}); diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 320870b3ee..dddac82c2d 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -29,7 +29,6 @@ import { } from '@backstage/backend-common'; import { assertError, NotFoundError } from '@backstage/errors'; import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; -import { Config } from '@backstage/config'; import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; import connectSessionKnex from 'connect-session-knex'; @@ -41,6 +40,8 @@ import { BACKSTAGE_SESSION_EXPIRATION } from '../lib/session'; import { TokenIssuer } from '../identity/types'; import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; +import { Config, readDurationFromConfig } from '@backstage/config'; +import { durationToMilliseconds } from '@backstage/types'; /** @public */ export type ProviderFactories = { [s: string]: AuthProviderFactory }; @@ -76,9 +77,8 @@ export async function createRouter( const appUrl = config.getString('app.baseUrl'); const authUrl = await discovery.getExternalBaseUrl('auth'); - + const backstageTokenExpiration = getDefaultBackstageTokenExpiryTime(config); const authDb = AuthDatabase.create(database); - const sessionExpirationSeconds = BACKSTAGE_SESSION_EXPIRATION; const keyStore = await KeyStores.fromConfig(config, { logger, @@ -91,7 +91,7 @@ export async function createRouter( { logger: logger.child({ component: 'token-factory' }), issuer: authUrl, - sessionExpirationSeconds: sessionExpirationSeconds, + sessionExpirationSeconds: backstageTokenExpiration, }, keyStore as StaticKeyStore, ); @@ -99,7 +99,7 @@ export async function createRouter( tokenIssuer = new TokenFactory({ issuer: authUrl, keyStore, - keyDurationSeconds: sessionExpirationSeconds, + keyDurationSeconds: backstageTokenExpiration, logger: logger.child({ component: 'token-factory' }), algorithm: tokenFactoryAlgorithm ?? @@ -249,3 +249,27 @@ export function createOriginFilter( return allowedOriginPatterns.some(pattern => pattern.match(origin)); }; } + +/** @internal */ +export function getDefaultBackstageTokenExpiryTime(config: Config) { + const processingIntervalKey = 'auth.backstageTokenExpiration'; + + if (!config.has(processingIntervalKey)) { + return BACKSTAGE_SESSION_EXPIRATION; + } + + const duration = readDurationFromConfig(config, { + key: processingIntervalKey, + }); + + const roundedDuration = Math.round(durationToMilliseconds(duration) / 1000); + + const minSeconds = Math.max(600, roundedDuration); + + const maxSeconds = Math.min(86400, roundedDuration); + + if (roundedDuration < minSeconds) { + return minSeconds + } + return maxSeconds; +} diff --git a/yarn.lock b/yarn.lock index d586cee159..60502016df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4908,6 +4908,7 @@ __metadata: "@backstage/plugin-auth-backend-module-okta-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/types": "workspace:^" "@google-cloud/firestore": ^7.0.0 "@types/body-parser": ^1.19.0 "@types/cookie-parser": ^1.4.2