mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-13 17:23:15 +03:00
feat: allow introspection and device code endpoints to use Federated Client Credentials (#640)
This commit is contained in:
committed by
GitHub
parent
df5c1ed1f8
commit
b62b61fb01
@@ -87,11 +87,13 @@ export const refreshTokens = [
|
||||
{
|
||||
token: 'ou87UDg249r1StBLYkMEqy9TXDbV5HmGuDpMcZDo',
|
||||
clientId: oidcClients.nextcloud.id,
|
||||
userId: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e',
|
||||
expired: false
|
||||
},
|
||||
{
|
||||
token: 'X4vqwtRyCUaq51UafHea4Fsg8Km6CAns6vp3tuX4',
|
||||
clientId: oidcClients.nextcloud.id,
|
||||
userId: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e',
|
||||
expired: true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -156,11 +156,21 @@ test("End session with id token hint redirects to callback URL", async ({
|
||||
test("Successfully refresh tokens with valid refresh token", async ({
|
||||
request,
|
||||
}) => {
|
||||
const { token, clientId } = refreshTokens.filter(
|
||||
const { token, clientId, userId } = refreshTokens.filter(
|
||||
(token) => !token.expired
|
||||
)[0];
|
||||
const clientSecret = "w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY";
|
||||
|
||||
// Sign the refresh token
|
||||
const refreshToken = await request.post("/api/test/refreshtoken", {
|
||||
data: {
|
||||
rt: token,
|
||||
client: clientId,
|
||||
user: userId,
|
||||
}
|
||||
}).then((r) => r.text())
|
||||
|
||||
// Perform the exchange
|
||||
const refreshResponse = await request.post("/api/oidc/token", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -168,7 +178,7 @@ test("Successfully refresh tokens with valid refresh token", async ({
|
||||
form: {
|
||||
grant_type: "refresh_token",
|
||||
client_id: clientId,
|
||||
refresh_token: token,
|
||||
refresh_token: refreshToken,
|
||||
client_secret: clientSecret,
|
||||
},
|
||||
});
|
||||
@@ -184,26 +194,25 @@ test("Successfully refresh tokens with valid refresh token", async ({
|
||||
expect(tokenData.refresh_token).not.toBe(token);
|
||||
});
|
||||
|
||||
test("Using refresh token invalidates it for future use", async ({
|
||||
|
||||
test("Refresh token fails when used for the wrong client", async ({
|
||||
request,
|
||||
}) => {
|
||||
const { token, clientId } = refreshTokens.filter(
|
||||
const { token, clientId, userId } = refreshTokens.filter(
|
||||
(token) => !token.expired
|
||||
)[0];
|
||||
const clientSecret = "w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY";
|
||||
|
||||
await request.post("/api/oidc/token", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
form: {
|
||||
grant_type: "refresh_token",
|
||||
client_id: clientId,
|
||||
refresh_token: token,
|
||||
client_secret: clientSecret,
|
||||
},
|
||||
});
|
||||
// Sign the refresh token
|
||||
const refreshToken = await request.post("/api/test/refreshtoken", {
|
||||
data: {
|
||||
rt: token,
|
||||
client: 'bad-client',
|
||||
user: userId,
|
||||
}
|
||||
}).then((r) => r.text())
|
||||
|
||||
// Perform the exchange
|
||||
const refreshResponse = await request.post("/api/oidc/token", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -211,7 +220,86 @@ test("Using refresh token invalidates it for future use", async ({
|
||||
form: {
|
||||
grant_type: "refresh_token",
|
||||
client_id: clientId,
|
||||
refresh_token: token,
|
||||
refresh_token: refreshToken,
|
||||
client_secret: clientSecret,
|
||||
},
|
||||
});
|
||||
|
||||
expect(refreshResponse.status()).toBe(400);
|
||||
});
|
||||
|
||||
test("Refresh token fails when used for the wrong user", async ({
|
||||
request,
|
||||
}) => {
|
||||
const { token, clientId } = refreshTokens.filter(
|
||||
(token) => !token.expired
|
||||
)[0];
|
||||
const clientSecret = "w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY";
|
||||
|
||||
// Sign the refresh token
|
||||
const refreshToken = await request.post("/api/test/refreshtoken", {
|
||||
data: {
|
||||
rt: token,
|
||||
client: clientId,
|
||||
user: 'bad-user',
|
||||
}
|
||||
}).then((r) => r.text())
|
||||
|
||||
// Perform the exchange
|
||||
const refreshResponse = await request.post("/api/oidc/token", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
form: {
|
||||
grant_type: "refresh_token",
|
||||
client_id: clientId,
|
||||
refresh_token: refreshToken,
|
||||
client_secret: clientSecret,
|
||||
},
|
||||
});
|
||||
|
||||
expect(refreshResponse.status()).toBe(400);
|
||||
});
|
||||
|
||||
test("Using refresh token invalidates it for future use", async ({
|
||||
request,
|
||||
}) => {
|
||||
const { token, clientId, userId } = refreshTokens.filter(
|
||||
(token) => !token.expired
|
||||
)[0];
|
||||
const clientSecret = "w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY";
|
||||
|
||||
// Sign the refresh token
|
||||
const refreshToken = await request.post("/api/test/refreshtoken", {
|
||||
data: {
|
||||
rt: token,
|
||||
client: clientId,
|
||||
user: userId,
|
||||
}
|
||||
}).then((r) => r.text())
|
||||
|
||||
// Perform the exchange
|
||||
await request.post("/api/oidc/token", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
form: {
|
||||
grant_type: "refresh_token",
|
||||
client_id: clientId,
|
||||
refresh_token: refreshToken,
|
||||
client_secret: clientSecret,
|
||||
},
|
||||
});
|
||||
|
||||
// Try again
|
||||
const refreshResponse = await request.post("/api/oidc/token", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
form: {
|
||||
grant_type: "refresh_token",
|
||||
client_id: clientId,
|
||||
refresh_token: refreshToken,
|
||||
client_secret: clientSecret,
|
||||
},
|
||||
});
|
||||
@@ -219,11 +307,10 @@ test("Using refresh token invalidates it for future use", async ({
|
||||
});
|
||||
|
||||
test.describe("Introspection endpoint", () => {
|
||||
const client = oidcClients.nextcloud;
|
||||
test("without client_id and client_secret fails", async ({ request }) => {
|
||||
test("fails without client credentials", async ({ request }) => {
|
||||
const validAccessToken = await generateOauthAccessToken(
|
||||
users.tim,
|
||||
client.id
|
||||
oidcClients.nextcloud.id
|
||||
);
|
||||
const introspectionResponse = await request.post("/api/oidc/introspect", {
|
||||
headers: {
|
||||
@@ -237,20 +324,20 @@ test.describe("Introspection endpoint", () => {
|
||||
expect(introspectionResponse.status()).toBe(400);
|
||||
});
|
||||
|
||||
test("with client_id and client_secret succeeds", async ({
|
||||
test("succeeds with client credentials", async ({
|
||||
request,
|
||||
baseURL,
|
||||
}) => {
|
||||
const validAccessToken = await generateOauthAccessToken(
|
||||
users.tim,
|
||||
client.id
|
||||
oidcClients.nextcloud.id
|
||||
);
|
||||
const introspectionResponse = await request.post("/api/oidc/introspect", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization:
|
||||
"Basic " +
|
||||
Buffer.from(`${client.id}:${client.secret}`).toString("base64"),
|
||||
Buffer.from(`${oidcClients.nextcloud.id}:${oidcClients.nextcloud.secret}`).toString("base64"),
|
||||
},
|
||||
form: {
|
||||
token: validAccessToken,
|
||||
@@ -266,18 +353,102 @@ test.describe("Introspection endpoint", () => {
|
||||
expect(introspectionBody.aud).toStrictEqual([oidcClients.nextcloud.id]);
|
||||
});
|
||||
|
||||
test("succeeds with federated client credentials", async ({
|
||||
page,
|
||||
request,
|
||||
baseURL,
|
||||
}) => {
|
||||
const validAccessToken = await generateOauthAccessToken(
|
||||
users.tim,
|
||||
oidcClients.federated.id
|
||||
);
|
||||
const clientAssertion = await oidcUtil.getClientAssertion(page, oidcClients.federated.federatedJWT);
|
||||
const introspectionResponse = await request.post("/api/oidc/introspect", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization: "Bearer " + clientAssertion,
|
||||
},
|
||||
form: {
|
||||
token: validAccessToken,
|
||||
},
|
||||
});
|
||||
|
||||
expect(introspectionResponse.status()).toBe(200);
|
||||
const introspectionBody = await introspectionResponse.json();
|
||||
expect(introspectionBody.active).toBe(true);
|
||||
expect(introspectionBody.token_type).toBe("access_token");
|
||||
expect(introspectionBody.iss).toBe(baseURL);
|
||||
expect(introspectionBody.sub).toBe(users.tim.id);
|
||||
expect(introspectionBody.aud).toStrictEqual([oidcClients.federated.id]);
|
||||
});
|
||||
|
||||
test("fails with client credentials for wrong app", async ({
|
||||
request,
|
||||
}) => {
|
||||
const validAccessToken = await generateOauthAccessToken(
|
||||
users.tim,
|
||||
oidcClients.nextcloud.id
|
||||
);
|
||||
const introspectionResponse = await request.post("/api/oidc/introspect", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization:
|
||||
"Basic " +
|
||||
Buffer.from(`${oidcClients.immich.id}:${oidcClients.immich.secret}`).toString("base64"),
|
||||
},
|
||||
form: {
|
||||
token: validAccessToken,
|
||||
},
|
||||
});
|
||||
|
||||
expect(introspectionResponse.status()).toBe(400);
|
||||
});
|
||||
|
||||
test("fails with federated credentials for wrong app", async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
const validAccessToken = await generateOauthAccessToken(
|
||||
users.tim,
|
||||
oidcClients.nextcloud.id
|
||||
);
|
||||
const clientAssertion = await oidcUtil.getClientAssertion(page, oidcClients.federated.federatedJWT);
|
||||
const introspectionResponse = await request.post("/api/oidc/introspect", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization: "Bearer " + clientAssertion,
|
||||
},
|
||||
form: {
|
||||
token: validAccessToken,
|
||||
},
|
||||
});
|
||||
|
||||
expect(introspectionResponse.status()).toBe(400);
|
||||
});
|
||||
|
||||
test("non-expired refresh_token can be verified", async ({ request }) => {
|
||||
const { token } = refreshTokens.filter((token) => !token.expired)[0];
|
||||
const { token, clientId, userId } = refreshTokens.filter(
|
||||
(token) => !token.expired
|
||||
)[0];
|
||||
|
||||
// Sign the refresh token
|
||||
const refreshToken = await request.post("/api/test/refreshtoken", {
|
||||
data: {
|
||||
rt: token,
|
||||
client: clientId,
|
||||
user: userId,
|
||||
}
|
||||
}).then((r) => r.text())
|
||||
|
||||
const introspectionResponse = await request.post("/api/oidc/introspect", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization:
|
||||
"Basic " +
|
||||
Buffer.from(`${client.id}:${client.secret}`).toString("base64"),
|
||||
Buffer.from(`${oidcClients.nextcloud.id}:${oidcClients.nextcloud.secret}`).toString("base64"),
|
||||
},
|
||||
form: {
|
||||
token: token,
|
||||
token: refreshToken,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -288,17 +459,28 @@ test.describe("Introspection endpoint", () => {
|
||||
});
|
||||
|
||||
test("expired refresh_token can be verified", async ({ request }) => {
|
||||
const { token } = refreshTokens.filter((token) => token.expired)[0];
|
||||
const { token, clientId, userId } = refreshTokens.filter(
|
||||
(token) => token.expired
|
||||
)[0];
|
||||
|
||||
// Sign the refresh token
|
||||
const refreshToken = await request.post("/api/test/refreshtoken", {
|
||||
data: {
|
||||
rt: token,
|
||||
client: clientId,
|
||||
user: userId,
|
||||
}
|
||||
}).then((r) => r.text())
|
||||
|
||||
const introspectionResponse = await request.post("/api/oidc/introspect", {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization:
|
||||
"Basic " +
|
||||
Buffer.from(`${client.id}:${client.secret}`).toString("base64"),
|
||||
Buffer.from(`${oidcClients.nextcloud.id}:${oidcClients.nextcloud.secret}`).toString("base64"),
|
||||
},
|
||||
form: {
|
||||
token: token,
|
||||
token: refreshToken,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -310,7 +492,7 @@ test.describe("Introspection endpoint", () => {
|
||||
test("expired access_token can't be verified", async ({ request }) => {
|
||||
const expiredAccessToken = await generateOauthAccessToken(
|
||||
users.tim,
|
||||
client.id,
|
||||
oidcClients.nextcloud.id,
|
||||
true
|
||||
);
|
||||
const introspectionResponse = await request.post("/api/oidc/introspect", {
|
||||
|
||||
Reference in New Issue
Block a user