Normalize OAuth token scopes
This commit is contained in:
parent
475ee59e13
commit
09373ccb4b
2 changed files with 45 additions and 9 deletions
24
src/oauth.ts
24
src/oauth.ts
|
|
@ -70,6 +70,14 @@ async function fetchJson<T>(input: string | URL, init: RequestInit, fallbackErro
|
|||
return (text ? JSON.parse(text) : {}) as T;
|
||||
}
|
||||
|
||||
function normalizeScopes(scope: string | string[] | undefined): string[] {
|
||||
if (Array.isArray(scope)) {
|
||||
return scope.map((value) => value.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
return scope?.split(/\s+/).filter(Boolean) ?? [];
|
||||
}
|
||||
|
||||
async function exchangeDiscordAuthorizationCode(options: {
|
||||
config: HeimdallConfig;
|
||||
code: string;
|
||||
|
|
@ -91,7 +99,7 @@ async function exchangeDiscordAuthorizationCode(options: {
|
|||
refresh_token?: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
scope: string;
|
||||
scope?: string | string[];
|
||||
}>(
|
||||
"https://discord.com/api/v10/oauth2/token",
|
||||
{
|
||||
|
|
@ -108,7 +116,7 @@ async function exchangeDiscordAuthorizationCode(options: {
|
|||
const tokenSet: OAuthTokenSet = {
|
||||
accessToken: tokenResponse.access_token,
|
||||
tokenType: tokenResponse.token_type,
|
||||
scope: tokenResponse.scope.split(/\s+/).filter(Boolean),
|
||||
scope: normalizeScopes(tokenResponse.scope),
|
||||
expiresAt: new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString(),
|
||||
raw: tokenResponse as unknown as Record<string, unknown>,
|
||||
};
|
||||
|
|
@ -142,7 +150,7 @@ async function exchangePatreonAuthorizationCode(options: {
|
|||
refresh_token?: string;
|
||||
token_type: string;
|
||||
expires_in?: number;
|
||||
scope?: string;
|
||||
scope?: string | string[];
|
||||
}>(
|
||||
"https://www.patreon.com/api/oauth2/token",
|
||||
{
|
||||
|
|
@ -158,7 +166,7 @@ async function exchangePatreonAuthorizationCode(options: {
|
|||
const tokenSet: OAuthTokenSet = {
|
||||
accessToken: tokenResponse.access_token,
|
||||
tokenType: tokenResponse.token_type,
|
||||
scope: tokenResponse.scope?.split(/\s+/).filter(Boolean) ?? [],
|
||||
scope: normalizeScopes(tokenResponse.scope),
|
||||
raw: tokenResponse as unknown as Record<string, unknown>,
|
||||
};
|
||||
|
||||
|
|
@ -188,7 +196,7 @@ async function exchangeTwitchAuthorizationCode(options: {
|
|||
refresh_token?: string;
|
||||
token_type: string;
|
||||
expires_in?: number;
|
||||
scope?: string;
|
||||
scope?: string | string[];
|
||||
}>(
|
||||
"https://id.twitch.tv/oauth2/token",
|
||||
{
|
||||
|
|
@ -210,7 +218,7 @@ async function exchangeTwitchAuthorizationCode(options: {
|
|||
const tokenSet: OAuthTokenSet = {
|
||||
accessToken: tokenResponse.access_token,
|
||||
tokenType: tokenResponse.token_type,
|
||||
scope: tokenResponse.scope?.split(/\s+/).filter(Boolean) ?? [],
|
||||
scope: normalizeScopes(tokenResponse.scope),
|
||||
raw: tokenResponse as unknown as Record<string, unknown>,
|
||||
};
|
||||
|
||||
|
|
@ -240,7 +248,7 @@ async function exchangeYouTubeAuthorizationCode(options: {
|
|||
refresh_token?: string;
|
||||
token_type: string;
|
||||
expires_in?: number;
|
||||
scope?: string;
|
||||
scope?: string | string[];
|
||||
id_token?: string;
|
||||
}>(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
|
|
@ -263,7 +271,7 @@ async function exchangeYouTubeAuthorizationCode(options: {
|
|||
const tokenSet: OAuthTokenSet = {
|
||||
accessToken: tokenResponse.access_token,
|
||||
tokenType: tokenResponse.token_type,
|
||||
scope: tokenResponse.scope?.split(/\s+/).filter(Boolean) ?? [],
|
||||
scope: normalizeScopes(tokenResponse.scope),
|
||||
raw: tokenResponse as unknown as Record<string, unknown>,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { type FastifyInstance } from "fastify";
|
|||
import { buildApp } from "../src/app.js";
|
||||
import { type HeimdallConfig } from "../src/config.js";
|
||||
import { entitlementFacts, grantFacts, identityFacts } from "../src/facts.js";
|
||||
import { type OAuthProviderRuntime } from "../src/oauth.js";
|
||||
import { createOAuthRuntimeRegistry, type OAuthProviderRuntime } from "../src/oauth.js";
|
||||
import { verifyJwt } from "../src/signing.js";
|
||||
import { createHeimdallAccessTokenVerifier } from "../src/verifier.js";
|
||||
import { InMemoryStore } from "../src/store/index.js";
|
||||
|
|
@ -655,4 +655,32 @@ describe("Heimdall service", () => {
|
|||
);
|
||||
expect(credentialResponse.body).not.toContain("twitch-refresh-token");
|
||||
});
|
||||
|
||||
it("accepts Twitch token scopes returned as an array", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
access_token: "twitch-access-token",
|
||||
refresh_token: "twitch-refresh-token",
|
||||
token_type: "bearer",
|
||||
expires_in: 14124,
|
||||
scope: ["user:read:email"],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const runtimes = createOAuthRuntimeRegistry();
|
||||
const tokenSet = await runtimes.twitch.exchangeAuthorizationCode({
|
||||
config: createTestConfig(),
|
||||
code: "test-code",
|
||||
redirectUri: "https://heimdall.gamecult.org/v1/oauth/twitch/callback",
|
||||
});
|
||||
|
||||
expect(tokenSet.scope).toEqual(["user:read:email"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue