Skip to content

Commit 5fd0188

Browse files
committed
Allow for missing offline_access scope
1 parent a626b90 commit 5fd0188

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { beforeEach, describe, expect, test, vi } from "vitest";
2+
import { createPinia, setActivePinia } from "pinia";
3+
import serviceControlClient from "@/components/serviceControlClient";
4+
import { useAuthStore } from "@/stores/AuthStore";
5+
6+
describe("AuthStore tests", () => {
7+
const baseResponse = {
8+
enabled: true,
9+
role_based_authorization_enabled: true,
10+
client_id: "test-client-id",
11+
authority: "https://login.example.com",
12+
api_scopes: JSON.stringify(["api://test-audience/.default"]),
13+
audience: "api://test-audience",
14+
};
15+
16+
beforeEach(() => {
17+
setActivePinia(createPinia());
18+
});
19+
20+
test("uses the composed scopes field from ServiceControl when present", async () => {
21+
vi.spyOn(serviceControlClient, "fetchTypedFromServiceControl").mockResolvedValue([
22+
{} as Response,
23+
{
24+
...baseResponse,
25+
scopes: "api://test-audience/.default openid profile email",
26+
},
27+
]);
28+
29+
const store = useAuthStore();
30+
await store.refresh();
31+
32+
expect(store.authConfig?.scope).toBe("api://test-audience/.default openid profile email");
33+
});
34+
35+
test("falls back to assembling scopes from api_scopes when talking to an older ServiceControl", async () => {
36+
// Older ServiceControl versions don't send the `scopes` field at all.
37+
vi.spyOn(serviceControlClient, "fetchTypedFromServiceControl").mockResolvedValue([{} as Response, { ...baseResponse }]);
38+
39+
const store = useAuthStore();
40+
await store.refresh();
41+
42+
expect(store.authConfig?.scope).toBe("api://test-audience/.default openid profile email offline_access");
43+
});
44+
});

src/Frontend/src/stores/AuthStore.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ interface AuthConfigResponse {
1414
authority: string;
1515
api_scopes: string;
1616
audience: string;
17+
// Complete scope string composed by ServiceControl (api scopes + openid profile email + offline_access,
18+
// the last omitted if the operator disabled it). Absent on ServiceControl versions older than the one
19+
// that introduced this field, in which case we fall back to assembling it ourselves below.
20+
scopes?: string;
1721
}
1822

1923
export const useAuthStore = defineStore("auth", () => {
@@ -52,7 +56,11 @@ export const useAuthStore = defineStore("auth", () => {
5256
}
5357

5458
function transformToAuthConfig(config: AuthConfigResponse): AuthConfig {
59+
// ServiceControl composes the full scope string (including whether offline_access is permitted
60+
// by the identity provider). Older ServiceControl versions don't send it, so fall back to the
61+
// previous behaviour of assembling it from api_scopes.
5562
const apiScope = JSON.parse(config.api_scopes).join(" ");
63+
const scope = config.scopes ?? `${apiScope} openid profile email offline_access`;
5664
// Use hash-based URL for post-logout redirect since the app uses hash routing
5765
const postLogoutRedirectUri = `${window.location.origin}${window.location.pathname}#${routeLinks.loggedOut}`;
5866
return {
@@ -61,7 +69,7 @@ export const useAuthStore = defineStore("auth", () => {
6169
redirect_uri: window.location.origin,
6270
post_logout_redirect_uri: postLogoutRedirectUri,
6371
response_type: "code",
64-
scope: `${apiScope} openid profile email offline_access`,
72+
scope,
6573
automaticSilentRenew: true,
6674
loadUserInfo: false,
6775
includeIdTokenInSilentRenew: true,

0 commit comments

Comments
 (0)