support user configuration of auth cookie max age

Signed-off-by: Jessica He <jhe@redhat.com>
This commit is contained in:
Jessica He
2024-11-25 18:49:51 -05:00
parent 59370518e6
commit 61f464e864
51 changed files with 245 additions and 9 deletions
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -38,6 +40,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -45,6 +45,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"supertest": "^7.0.0"
},
"configSchema": "config.d.ts"
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -30,6 +32,7 @@ export interface Config {
audience?: string;
connection?: string;
connectionScope?: string;
sessionDuration?: HumanDuration | string;
};
};
};
@@ -45,6 +45,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"@types/passport-auth0": "^1.0.5",
"@types/passport-oauth2": "^1.4.15",
"supertest": "^7.0.0"
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -48,6 +50,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -48,6 +48,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/types": "workspace:^",
"express": "^4.18.2",
"msw": "^2.0.8"
}
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -36,6 +38,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -45,6 +45,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"supertest": "^7.0.0"
},
"configSchema": "config.d.ts"
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -27,6 +29,7 @@ export interface Config {
clientSecret: string;
host: string;
callbackUrl?: string;
sessionDuration?: HumanDuration | string;
};
};
};
@@ -44,6 +44,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"@types/passport-oauth2": "^1.4.15",
"supertest": "^7.0.0"
},
@@ -43,6 +43,7 @@ export interface Config {
* The backstage token expiration.
*/
backstageTokenExpiration?: HumanDuration | string;
sessionDuration?: HumanDuration | string;
};
};
}
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -43,6 +45,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -38,6 +40,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -43,6 +43,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"supertest": "^7.0.0"
},
"configSchema": "config.d.ts"
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -38,6 +40,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -45,6 +45,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"supertest": "^7.0.0"
},
"configSchema": "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?: {
@@ -38,6 +40,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -47,6 +47,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"@types/passport-google-oauth20": "^2.0.3",
"supertest": "^7.0.0"
},
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -40,6 +42,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -46,6 +46,7 @@
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"@types/passport-microsoft": "^1.0.0",
"msw": "^1.0.0",
"supertest": "^7.0.0"
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -42,6 +44,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -44,6 +44,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"supertest": "^7.0.0"
},
"configSchema": "config.d.ts"
+3
View File
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -40,6 +42,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -46,6 +46,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/types": "workspace:^",
"cookie-parser": "^1.4.6",
"express-promise-router": "^4.1.1",
"express-session": "^1.17.3",
+3
View File
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -40,6 +42,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -45,6 +45,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"supertest": "^7.0.0"
},
"configSchema": "config.d.ts"
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -37,6 +39,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -45,6 +45,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"supertest": "^7.0.0"
},
"configSchema": "config.d.ts"
@@ -36,6 +36,7 @@
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/types": "workspace:^",
"luxon": "^3.4.3",
"openid-client": "^5.4.3"
},
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
/** Configuration options for the auth plugin */
auth?: {
@@ -27,6 +29,7 @@ export interface Config {
*/
clientSecret: string;
scope?: string;
sessionDuration?: HumanDuration | string;
};
};
};
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { HumanDuration } from '@backstage/types';
export interface Config {
auth?: {
providers?: {
@@ -34,6 +36,7 @@ export interface Config {
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
};
};
};
@@ -47,6 +47,7 @@
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/types": "workspace:^",
"msw": "^2.0.8",
"supertest": "^7.0.0"
}
@@ -16,6 +16,7 @@
import { CookieOptions, Request, Response } from 'express';
import { CookieConfigurer } from '../types';
import { HumanDuration, durationToMilliseconds } from '@backstage/types';
const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
const TEN_MINUTES_MS = 600 * 1000;
@@ -55,6 +56,7 @@ export class OAuthCookieManager {
private readonly nonceCookie: string;
private readonly refreshTokenCookie: string;
private readonly grantedScopeCookie: string;
private readonly maxAge: number;
constructor(
private readonly options: {
@@ -63,6 +65,7 @@ export class OAuthCookieManager {
baseUrl: string;
callbackUrl: string;
cookieConfigurer?: CookieConfigurer;
sessionDuration?: HumanDuration;
},
) {
this.cookieConfigurer = options.cookieConfigurer ?? defaultCookieConfigurer;
@@ -70,6 +73,9 @@ export class OAuthCookieManager {
this.nonceCookie = `${options.providerId}-nonce`;
this.refreshTokenCookie = `${options.providerId}-refresh-token`;
this.grantedScopeCookie = `${options.providerId}-granted-scope`;
this.maxAge = options.sessionDuration
? durationToMilliseconds(options.sessionDuration)
: THOUSAND_DAYS_MS;
}
private getConfig(origin?: string, pathSuffix: string = '') {
@@ -103,7 +109,7 @@ export class OAuthCookieManager {
res,
this.refreshTokenCookie,
refreshToken,
THOUSAND_DAYS_MS,
this.maxAge,
origin,
);
}
@@ -117,13 +123,7 @@ export class OAuthCookieManager {
}
setGrantedScopes(res: Response, scope: string, origin?: string): void {
this.setCookie(
res,
this.grantedScopeCookie,
scope,
THOUSAND_DAYS_MS,
origin,
);
this.setCookie(res, this.grantedScopeCookie, scope, this.maxAge, origin);
}
getNonce(req: Request): string | undefined {
@@ -813,6 +813,75 @@ describe('createOAuthRouteHandlers', () => {
},
});
});
it('should set sessionDuration to configured value', async () => {
const baseConfigWithSessionDuration = {
...baseConfig,
config: new ConfigReader({
sessionDuration: { days: 7 },
}),
};
const agent = request.agent(
wrapInApp(createOAuthRouteHandlers(baseConfigWithSessionDuration)),
);
agent.jar.setCookie(
'my-provider-refresh-token=refresh-token',
'127.0.0.1',
'/my-provider',
);
mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({
fullProfile: { id: 'id' } as PassportProfile,
session: { ...mockSession, scope, refreshToken: 'new-refresh-token' },
}));
const res = await agent
.post('/my-provider/refresh')
.set('X-Requested-With', 'XMLHttpRequest');
expect(res.status).toBe(200);
const expectedExpirationDate = Date.now() + 7 * 24 * 60 * 60 * 1000;
const cookie = getRefreshTokenCookie(agent);
expect(cookie.expiration_date).toBeGreaterThanOrEqual(
expectedExpirationDate - 1000,
);
expect(cookie.expiration_date).toBeLessThanOrEqual(
expectedExpirationDate + 1000,
);
});
it('should set sessionDuration to default of 1000 days when not configured', async () => {
const agent = request.agent(
wrapInApp(createOAuthRouteHandlers(baseConfig)),
);
agent.jar.setCookie(
'my-provider-refresh-token=refresh-token',
'127.0.0.1',
'/my-provider',
);
mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({
fullProfile: { id: 'id' } as PassportProfile,
session: { ...mockSession, scope, refreshToken: 'new-refresh-token' },
}));
const res = await agent
.post('/my-provider/refresh')
.set('X-Requested-With', 'XMLHttpRequest');
expect(res.status).toBe(200);
const expectedExpirationDate = Date.now() + 1000 * 24 * 60 * 60 * 1000;
const cookie = getRefreshTokenCookie(agent);
expect(cookie.expiration_date).toBeGreaterThanOrEqual(
expectedExpirationDate - 1000,
);
expect(cookie.expiration_date).toBeLessThanOrEqual(
expectedExpirationDate + 1000,
);
});
});
describe('logout', () => {
@@ -40,7 +40,7 @@ import {
SignInResolver,
} from '../types';
import { OAuthAuthenticator, OAuthAuthenticatorResult } from './types';
import { Config } from '@backstage/config';
import { Config, readDurationFromConfig } from '@backstage/config';
import { CookieScopeManager } from './CookieScopeManager';
/** @public */
@@ -99,6 +99,9 @@ export function createOAuthRouteHandlers<TProfile>(
const callbackUrl =
config.getOptionalString('callbackUrl') ??
`${baseUrl}/${providerId}/handler/frame`;
const sessionDuration = config.has('sessionDuration')
? readDurationFromConfig(config, { key: 'sessionDuration' })
: undefined;
const stateTransform = options.stateTransform ?? (state => ({ state }));
const profileTransform =
@@ -110,6 +113,7 @@ export function createOAuthRouteHandlers<TProfile>(
defaultAppOrigin,
providerId,
cookieConfigurer,
sessionDuration,
});
const scopeManager = CookieScopeManager.create({