mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-07 02:07:53 +02:00
🛂(backend) add audience to jwt
Add audience to the jwt, scoping the token to it prevents an admin JWT issued for another backend service from being replayed against y-provider.
This commit is contained in:
@@ -13,6 +13,11 @@ from core.services.jwt_services import JWTService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Audience of the admin token y-provider expects. Scoping the token to it
|
||||
# prevents an admin JWT issued for another backend service from being
|
||||
# replayed against y-provider.
|
||||
Y_CONVERTER_AUDIENCE = "y-converter"
|
||||
|
||||
|
||||
class ConversionError(Exception):
|
||||
"""Base exception for conversion-related errors."""
|
||||
@@ -110,7 +115,8 @@ class YdocConverter:
|
||||
@property
|
||||
def auth_header(self):
|
||||
"""Build microservice authentication header."""
|
||||
return f"Bearer {JWTService().get_admin_token()}"
|
||||
token = JWTService().get_admin_token({"aud": Y_CONVERTER_AUDIENCE})
|
||||
return f"Bearer {token}"
|
||||
|
||||
def _request(self, url, data, content_type, accept):
|
||||
"""Make a request to the Y-Provider API."""
|
||||
|
||||
@@ -27,14 +27,17 @@ def jwt_settings(settings):
|
||||
|
||||
|
||||
def test_auth_header():
|
||||
"""The auth header carries an admin JWT signed with the configured key."""
|
||||
"""The auth header carries an admin JWT scoped to the y-converter audience."""
|
||||
converter = YdocConverter()
|
||||
|
||||
scheme, token = converter.auth_header.split(" ")
|
||||
|
||||
assert scheme == "Bearer"
|
||||
payload = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])
|
||||
payload = jwt.decode(
|
||||
token, PUBLIC_KEY, algorithms=["RS256"], audience="y-converter"
|
||||
)
|
||||
assert payload["admin"] is True
|
||||
assert payload["aud"] == "y-converter"
|
||||
|
||||
|
||||
def test_convert_empty_text():
|
||||
|
||||
@@ -18,6 +18,7 @@ import { httpSecurity } from '@/middlewares';
|
||||
import {
|
||||
mockJwksEndpoint,
|
||||
signAdminToken,
|
||||
signAdminTokenForAudience,
|
||||
signAdminTokenWithWrongKey,
|
||||
signExpiredAdminToken,
|
||||
signToken,
|
||||
@@ -88,6 +89,19 @@ describe('httpSecurity', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a valid admin JWT issued for another audience', async () => {
|
||||
const token = await signAdminTokenForAudience('some-other-service');
|
||||
|
||||
const response = await request(buildApp())
|
||||
.get('/protected')
|
||||
.set('authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toStrictEqual({
|
||||
error: 'Unauthorized: Invalid API Key',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a validly signed JWT missing the admin claim', async () => {
|
||||
const token = await signToken({ sub: 'someone' });
|
||||
|
||||
|
||||
@@ -27,7 +27,12 @@ export const signToken = (claims: Record<string, unknown>) =>
|
||||
.setExpirationTime('1h')
|
||||
.sign(privateKey);
|
||||
|
||||
export const signAdminToken = () => signToken({ admin: true });
|
||||
export const signAdminToken = () =>
|
||||
signToken({ admin: true, aud: 'y-converter' });
|
||||
|
||||
/** An admin token correctly signed but scoped to another service's audience. */
|
||||
export const signAdminTokenForAudience = (aud: string) =>
|
||||
signToken({ admin: true, aud });
|
||||
|
||||
/** An admin token signed correctly but already past its expiry. */
|
||||
export const signExpiredAdminToken = () =>
|
||||
|
||||
@@ -16,14 +16,21 @@ export const corsMiddleware = cors({
|
||||
// keeps them until their "kid" no longer matches a token, per jose's own policy.
|
||||
const jwks = createRemoteJWKSet(new URL(JWKS_URL));
|
||||
|
||||
// Requiring this audience stops a valid admin JWT issued for another service
|
||||
// from being replayed against y-provider.
|
||||
const Y_CONVERTER_AUDIENCE = 'y-converter';
|
||||
export const JWT_ALGORITHM = 'RS256';
|
||||
|
||||
/**
|
||||
* Verify that the given token is an admin JWT signed by the Django backend.
|
||||
* Verify that the given token is an admin JWT signed by the Django backend
|
||||
* for the y-converter audience.
|
||||
*/
|
||||
const isValidAdminToken = async (token: string): Promise<boolean> => {
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, jwks, { algorithms: [JWT_ALGORITHM] });
|
||||
const { payload } = await jwtVerify(token, jwks, {
|
||||
algorithms: [JWT_ALGORITHM],
|
||||
audience: Y_CONVERTER_AUDIENCE,
|
||||
});
|
||||
return payload.admin === true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user