auth-node: add scopeAlreadyGranted field

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-12 12:13:12 +01:00
parent 8dee48768c
commit ab9a6fb321
7 changed files with 84 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-node': patch
---
Added `scopeAlreadyGranted` property to `OAuthAuthenticatorRefreshInput`, signaling to the provider whether the requested scope has already been granted when persisting session scope.
+1
View File
@@ -326,6 +326,7 @@ export interface OAuthAuthenticatorRefreshInput {
req: Request_2;
// (undocumented)
scope: string;
scopeAlreadyGranted?: boolean;
}
// @public (undocumented)
@@ -193,6 +193,55 @@ describe('CookieScopeManager', () => {
);
});
it('should signal whether persisted scopes have already been granted when refreshing', async () => {
const getGrantedScopes = jest.fn();
const manager = CookieScopeManager.create({
authenticator: {
scopes: {
persist: true,
} as OAuthAuthenticatorScopeOptions,
} as OAuthAuthenticator<any, any>,
cookieManager: {
getGrantedScopes,
} as unknown as OAuthCookieManager,
});
getGrantedScopes.mockReturnValue('x y');
await expect(manager.refresh(makeReq('x,y'))).resolves.toEqual({
scope: 'x y',
scopeAlreadyGranted: true,
commit: expect.any(Function),
});
getGrantedScopes.mockReturnValueOnce('x y');
await expect(manager.refresh(makeReq('x'))).resolves.toEqual({
scope: 'x y',
scopeAlreadyGranted: true,
commit: expect.any(Function),
});
getGrantedScopes.mockReturnValueOnce('x y');
await expect(manager.refresh(makeReq('x,y,z'))).resolves.toEqual({
scope: 'x y z',
scopeAlreadyGranted: false,
commit: expect.any(Function),
});
getGrantedScopes.mockReturnValueOnce('');
await expect(manager.refresh(makeReq('x,y'))).resolves.toEqual({
scope: 'x y',
scopeAlreadyGranted: false,
commit: expect.any(Function),
});
getGrantedScopes.mockReturnValueOnce(undefined);
await expect(manager.refresh(makeReq('x,y'))).resolves.toEqual({
scope: 'x y',
scopeAlreadyGranted: false,
commit: expect.any(Function),
});
});
it('should use custom scope transform', async () => {
const manager = CookieScopeManager.create({
additionalScopes: ['b'],
@@ -138,6 +138,7 @@ export class CookieScopeManager {
async refresh(req: express.Request): Promise<{
scope: string;
scopeAlreadyGranted?: boolean;
commit(result: OAuthAuthenticatorResult<any>): Promise<string>;
}> {
const requestScope = splitScope(req.query.scope?.toString());
@@ -147,6 +148,9 @@ export class CookieScopeManager {
return {
scope,
scopeAlreadyGranted: this.cookieManager
? hasScopeBeenGranted(grantedScope, scope)
: undefined,
commit: async result => {
if (this.cookieManager) {
this.cookieManager.setGrantedScopes(
@@ -162,3 +166,16 @@ export class CookieScopeManager {
};
}
}
function hasScopeBeenGranted(
grantedScope: Iterable<string>,
requestedScope: string,
): boolean {
const granted = new Set(grantedScope);
for (const requested of splitScope(requestedScope)) {
if (!granted.has(requested)) {
return false;
}
}
return true;
}
@@ -720,6 +720,7 @@ describe('createOAuthRouteHandlers', () => {
req: expect.anything(),
refreshToken: 'refresh-token',
scope: 'persisted-scope',
scopeAlreadyGranted: true,
},
{ ctx: 'authenticator' },
);
@@ -315,7 +315,12 @@ export function createOAuthRouteHandlers<TProfile>(
const scopeRefresh = await scopeManager.refresh(req);
const result = await authenticator.refresh(
{ req, scope: scopeRefresh.scope, refreshToken },
{
req,
scope: scopeRefresh.scope,
scopeAlreadyGranted: scopeRefresh.scopeAlreadyGranted,
refreshToken,
},
authenticatorCtx,
);
+5
View File
@@ -59,6 +59,11 @@ export interface OAuthAuthenticatorAuthenticateInput {
/** @public */
export interface OAuthAuthenticatorRefreshInput {
/**
* Signals whether the requested scope has already been granted for the session. Will only be set if the `scopes.persist` option is enabled.
*/
scopeAlreadyGranted?: boolean;
scope: string;
refreshToken: string;
req: Request;