[auth] Adjust scope management to line up with SEP-835 (#1133)

Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
This commit is contained in:
Paul Carleton 2025-11-18 17:30:50 +00:00 • committed by GitHub
commit 2e67eb552e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 210 additions and 10 deletions

View file

@ -316,6 +316,71 @@ describe('requireBearerAuth middleware', () => {
expect(nextFunction).not.toHaveBeenCalled();
});
describe('with requiredScopes in WWW-Authenticate header', () => {
it('should include scope in WWW-Authenticate header for 401 responses when requiredScopes is provided', async () => {
mockRequest.headers = {};
const middleware = requireBearerAuth({
verifier: mockVerifier,
requiredScopes: ['read', 'write']
});
await middleware(mockRequest as Request, mockResponse as Response, nextFunction);
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.set).toHaveBeenCalledWith(
'WWW-Authenticate',
'Bearer error="invalid_token", error_description="Missing Authorization header", scope="read write"'
);
expect(nextFunction).not.toHaveBeenCalled();
});
it('should include scope in WWW-Authenticate header for 403 insufficient scope responses', async () => {
const authInfo: AuthInfo = {
token: 'valid-token',
clientId: 'client-123',
scopes: ['read']
};
mockVerifyAccessToken.mockResolvedValue(authInfo);
mockRequest.headers = {
authorization: 'Bearer valid-token'
};
const middleware = requireBearerAuth({
verifier: mockVerifier,
requiredScopes: ['read', 'write']
});
await middleware(mockRequest as Request, mockResponse as Response, nextFunction);
expect(mockResponse.status).toHaveBeenCalledWith(403);
expect(mockResponse.set).toHaveBeenCalledWith(
'WWW-Authenticate',
'Bearer error="insufficient_scope", error_description="Insufficient scope", scope="read write"'
);
expect(nextFunction).not.toHaveBeenCalled();
});
it('should include both scope and resource_metadata in WWW-Authenticate header when both are provided', async () => {
mockRequest.headers = {};
const resourceMetadataUrl = 'https://api.example.com/.well-known/oauth-protected-resource';
const middleware = requireBearerAuth({
verifier: mockVerifier,
requiredScopes: ['admin'],
resourceMetadataUrl
});
await middleware(mockRequest as Request, mockResponse as Response, nextFunction);
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.set).toHaveBeenCalledWith(
'WWW-Authenticate',
`Bearer error="invalid_token", error_description="Missing Authorization header", scope="admin", resource_metadata="${resourceMetadataUrl}"`
);
expect(nextFunction).not.toHaveBeenCalled();
});
});
describe('with resourceMetadataUrl', () => {
const resourceMetadataUrl = 'https://api.example.com/.well-known/oauth-protected-resource';
@ -416,7 +481,7 @@ describe('requireBearerAuth middleware', () => {
expect(mockResponse.status).toHaveBeenCalledWith(403);
expect(mockResponse.set).toHaveBeenCalledWith(
'WWW-Authenticate',
`Bearer error="insufficient_scope", error_description="Insufficient scope", resource_metadata="${resourceMetadataUrl}"`
`Bearer error="insufficient_scope", error_description="Insufficient scope", scope="read write", resource_metadata="${resourceMetadataUrl}"`
);
expect(nextFunction).not.toHaveBeenCalled();
});

View file

@ -71,17 +71,23 @@ export function requireBearerAuth({ verifier, requiredScopes = [], resourceMetad
req.auth = authInfo;
next();
} catch (error) {
// Build WWW-Authenticate header parts
const buildWwwAuthHeader = (errorCode: string, message: string): string => {
let header = `Bearer error="${errorCode}", error_description="${message}"`;
if (requiredScopes.length > 0) {
header += `, scope="${requiredScopes.join(' ')}"`;
}
if (resourceMetadataUrl) {
header += `, resource_metadata="${resourceMetadataUrl}"`;
}
return header;
};
if (error instanceof InvalidTokenError) {
const wwwAuthValue = resourceMetadataUrl
? `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`
: `Bearer error="${error.errorCode}", error_description="${error.message}"`;
res.set('WWW-Authenticate', wwwAuthValue);
res.set('WWW-Authenticate', buildWwwAuthHeader(error.errorCode, error.message));
res.status(401).json(error.toResponseObject());
} else if (error instanceof InsufficientScopeError) {
const wwwAuthValue = resourceMetadataUrl
? `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`
: `Bearer error="${error.errorCode}", error_description="${error.message}"`;
res.set('WWW-Authenticate', wwwAuthValue);
res.set('WWW-Authenticate', buildWwwAuthHeader(error.errorCode, error.message));
res.status(403).json(error.toResponseObject());
} else if (error instanceof ServerError) {
res.status(500).json(error.toResponseObject());