You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Switch OIDC primarily to new /auth_metadata
API (#4626)
This commit is contained in:
committed by
GitHub
parent
61375ef38a
commit
c0e30ceca0
@ -36,7 +36,7 @@ import {
|
|||||||
MatrixError,
|
MatrixError,
|
||||||
MatrixHttpApi,
|
MatrixHttpApi,
|
||||||
} from "../../../src";
|
} from "../../../src";
|
||||||
import { mockOpenIdConfiguration } from "../../test-utils/oidc";
|
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||||
|
|
||||||
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
|
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
|
||||||
const baseUrl = "https://example.com";
|
const baseUrl = "https://example.com";
|
||||||
@ -57,7 +57,7 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
|
|||||||
getDomain: () => "example.com",
|
getDomain: () => "example.com",
|
||||||
getDevice: jest.fn(),
|
getDevice: jest.fn(),
|
||||||
getCrypto: jest.fn(() => crypto),
|
getCrypto: jest.fn(() => crypto),
|
||||||
getAuthIssuer: jest.fn().mockResolvedValue({ issuer: "https://issuer/" }),
|
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
|
||||||
} as unknown as MatrixClient;
|
} as unknown as MatrixClient;
|
||||||
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
||||||
baseUrl: client.baseUrl,
|
baseUrl: client.baseUrl,
|
||||||
@ -69,10 +69,6 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
|
|||||||
|
|
||||||
describe("MSC4108SignInWithQR", () => {
|
describe("MSC4108SignInWithQR", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.get(
|
|
||||||
"https://issuer/.well-known/openid-configuration",
|
|
||||||
mockOpenIdConfiguration("https://issuer/", [DEVICE_CODE_SCOPE]),
|
|
||||||
);
|
|
||||||
fetchMock.get("https://issuer/jwks", {
|
fetchMock.get("https://issuer/jwks", {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -14,42 +14,4 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
|
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../src/testing.ts";
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a valid OidcClientConfig with minimum valid values
|
|
||||||
* @param issuer used as the base for all other urls
|
|
||||||
* @returns OidcClientConfig
|
|
||||||
*/
|
|
||||||
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
|
|
||||||
const metadata = mockOpenIdConfiguration(issuer);
|
|
||||||
|
|
||||||
return {
|
|
||||||
accountManagementEndpoint: issuer + "account",
|
|
||||||
registrationEndpoint: metadata.registration_endpoint,
|
|
||||||
authorizationEndpoint: metadata.authorization_endpoint,
|
|
||||||
tokenEndpoint: metadata.token_endpoint,
|
|
||||||
metadata,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Useful for mocking <issuer>/.well-known/openid-configuration
|
|
||||||
* @param issuer used as the base for all other urls
|
|
||||||
* @returns ValidatedIssuerMetadata
|
|
||||||
*/
|
|
||||||
export const mockOpenIdConfiguration = (
|
|
||||||
issuer = "https://auth.org/",
|
|
||||||
additionalGrantTypes: string[] = [],
|
|
||||||
): ValidatedIssuerMetadata => ({
|
|
||||||
issuer,
|
|
||||||
revocation_endpoint: issuer + "revoke",
|
|
||||||
token_endpoint: issuer + "token",
|
|
||||||
authorization_endpoint: issuer + "auth",
|
|
||||||
registration_endpoint: issuer + "registration",
|
|
||||||
device_authorization_endpoint: issuer + "device",
|
|
||||||
jwks_uri: issuer + "jwks",
|
|
||||||
response_types_supported: ["code"],
|
|
||||||
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
|
|
||||||
code_challenge_methods_supported: ["S256"],
|
|
||||||
});
|
|
||||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Mocked, mocked } from "jest-mock";
|
import { Mocked, mocked } from "jest-mock";
|
||||||
|
import fetchMock from "fetch-mock-jest";
|
||||||
|
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
|
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
|
||||||
@ -76,6 +77,7 @@ import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from ".
|
|||||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||||
import { KnownMembership } from "../../src/@types/membership";
|
import { KnownMembership } from "../../src/@types/membership";
|
||||||
import { RoomMessageEventContent } from "../../src/@types/events";
|
import { RoomMessageEventContent } from "../../src/@types/events";
|
||||||
|
import { mockOpenIdConfiguration } from "../test-utils/oidc.ts";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
@ -265,13 +267,17 @@ describe("MatrixClient", function () {
|
|||||||
|
|
||||||
if (next.error) {
|
if (next.error) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
return Promise.reject({
|
return Promise.reject(
|
||||||
errcode: (<MatrixError>next.error).errcode,
|
new MatrixError(
|
||||||
httpStatus: (<MatrixError>next.error).httpStatus,
|
{
|
||||||
name: (<MatrixError>next.error).errcode,
|
errcode: (<MatrixError>next.error).errcode,
|
||||||
message: "Expected testing error",
|
name: (<MatrixError>next.error).errcode,
|
||||||
data: next.error,
|
message: "Expected testing error",
|
||||||
});
|
data: next.error,
|
||||||
|
},
|
||||||
|
(<MatrixError>next.error).httpStatus,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Promise.resolve(next.data);
|
return Promise.resolve(next.data);
|
||||||
}
|
}
|
||||||
@ -3489,6 +3495,63 @@ describe("MatrixClient", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getAuthMetadata", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchMock.mockReset();
|
||||||
|
// This request is made by oidc-client-ts so is not intercepted by httpLookups
|
||||||
|
fetchMock.get("https://auth.org/jwks", {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
keys: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use unstable prefix", async () => {
|
||||||
|
const metadata = mockOpenIdConfiguration();
|
||||||
|
httpLookups = [
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/auth_metadata`,
|
||||||
|
data: metadata,
|
||||||
|
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await expect(client.getAuthMetadata()).resolves.toEqual({
|
||||||
|
...metadata,
|
||||||
|
signingKeys: [],
|
||||||
|
});
|
||||||
|
expect(httpLookups.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to auth_issuer + openid-configuration", async () => {
|
||||||
|
const metadata = mockOpenIdConfiguration();
|
||||||
|
httpLookups = [
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/auth_metadata`,
|
||||||
|
error: new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404),
|
||||||
|
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/auth_issuer`,
|
||||||
|
data: { issuer: metadata.issuer },
|
||||||
|
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
fetchMock.get("https://auth.org/.well-known/openid-configuration", metadata);
|
||||||
|
|
||||||
|
await expect(client.getAuthMetadata()).resolves.toEqual({
|
||||||
|
...metadata,
|
||||||
|
signingKeys: [],
|
||||||
|
});
|
||||||
|
expect(httpLookups.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("identityHashedLookup", () => {
|
describe("identityHashedLookup", () => {
|
||||||
it("should return hashed lookup results", async () => {
|
it("should return hashed lookup results", async () => {
|
||||||
const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request";
|
const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request";
|
||||||
|
@ -40,8 +40,8 @@ jest.mock("jwt-decode");
|
|||||||
|
|
||||||
describe("oidc authorization", () => {
|
describe("oidc authorization", () => {
|
||||||
const delegatedAuthConfig = makeDelegatedAuthConfig();
|
const delegatedAuthConfig = makeDelegatedAuthConfig();
|
||||||
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
|
const authorizationEndpoint = delegatedAuthConfig.authorization_endpoint;
|
||||||
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
|
const tokenEndpoint = delegatedAuthConfig.token_endpoint;
|
||||||
const clientId = "xyz789";
|
const clientId = "xyz789";
|
||||||
const baseUrl = "https://test.com";
|
const baseUrl = "https://test.com";
|
||||||
|
|
||||||
@ -52,10 +52,7 @@ describe("oidc authorization", () => {
|
|||||||
jest.spyOn(logger, "warn");
|
jest.spyOn(logger, "warn");
|
||||||
jest.setSystemTime(now);
|
jest.setSystemTime(now);
|
||||||
|
|
||||||
fetchMock.get(
|
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
|
||||||
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
|
|
||||||
mockOpenIdConfiguration(),
|
|
||||||
);
|
|
||||||
globalThis.TextEncoder = TextEncoder;
|
globalThis.TextEncoder = TextEncoder;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -127,11 +124,9 @@ describe("oidc authorization", () => {
|
|||||||
it("should generate url with correct parameters", async () => {
|
it("should generate url with correct parameters", async () => {
|
||||||
const nonce = "abc123";
|
const nonce = "abc123";
|
||||||
|
|
||||||
const metadata = delegatedAuthConfig.metadata;
|
|
||||||
|
|
||||||
const authUrl = new URL(
|
const authUrl = new URL(
|
||||||
await generateOidcAuthorizationUrl({
|
await generateOidcAuthorizationUrl({
|
||||||
metadata,
|
metadata: delegatedAuthConfig,
|
||||||
homeserverUrl: baseUrl,
|
homeserverUrl: baseUrl,
|
||||||
clientId,
|
clientId,
|
||||||
redirectUri: baseUrl,
|
redirectUri: baseUrl,
|
||||||
@ -156,11 +151,9 @@ describe("oidc authorization", () => {
|
|||||||
it("should generate url with create prompt", async () => {
|
it("should generate url with create prompt", async () => {
|
||||||
const nonce = "abc123";
|
const nonce = "abc123";
|
||||||
|
|
||||||
const metadata = delegatedAuthConfig.metadata;
|
|
||||||
|
|
||||||
const authUrl = new URL(
|
const authUrl = new URL(
|
||||||
await generateOidcAuthorizationUrl({
|
await generateOidcAuthorizationUrl({
|
||||||
metadata,
|
metadata: delegatedAuthConfig,
|
||||||
homeserverUrl: baseUrl,
|
homeserverUrl: baseUrl,
|
||||||
clientId,
|
clientId,
|
||||||
redirectUri: baseUrl,
|
redirectUri: baseUrl,
|
||||||
|
@ -42,13 +42,13 @@ describe("registerOidcClient()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should make correct request to register client", async () => {
|
it("should make correct request to register client", async () => {
|
||||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: JSON.stringify({ client_id: dynamicClientId }),
|
body: JSON.stringify({ client_id: dynamicClientId }),
|
||||||
});
|
});
|
||||||
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
|
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
|
||||||
expect(fetchMockJest).toHaveBeenCalledWith(
|
expect(fetchMockJest).toHaveBeenCalledWith(
|
||||||
delegatedAuthConfig.registrationEndpoint!,
|
delegatedAuthConfig.registration_endpoint,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
@ -72,7 +72,7 @@ describe("registerOidcClient()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throw when registration request fails", async () => {
|
it("should throw when registration request fails", async () => {
|
||||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
|
||||||
status: 500,
|
status: 500,
|
||||||
});
|
});
|
||||||
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
|
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
|
||||||
@ -81,7 +81,7 @@ describe("registerOidcClient()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throw when registration response is invalid", async () => {
|
it("should throw when registration response is invalid", async () => {
|
||||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
|
||||||
status: 200,
|
status: 200,
|
||||||
// no clientId in response
|
// no clientId in response
|
||||||
body: "{}",
|
body: "{}",
|
||||||
@ -96,7 +96,7 @@ describe("registerOidcClient()", () => {
|
|||||||
registerOidcClient(
|
registerOidcClient(
|
||||||
{
|
{
|
||||||
...delegatedAuthConfig,
|
...delegatedAuthConfig,
|
||||||
registrationEndpoint: undefined,
|
registration_endpoint: undefined,
|
||||||
},
|
},
|
||||||
metadata,
|
metadata,
|
||||||
),
|
),
|
||||||
@ -108,10 +108,7 @@ describe("registerOidcClient()", () => {
|
|||||||
registerOidcClient(
|
registerOidcClient(
|
||||||
{
|
{
|
||||||
...delegatedAuthConfig,
|
...delegatedAuthConfig,
|
||||||
metadata: {
|
grant_types_supported: [delegatedAuthConfig.grant_types_supported[0]],
|
||||||
...delegatedAuthConfig.metadata,
|
|
||||||
grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
metadata,
|
metadata,
|
||||||
),
|
),
|
||||||
|
@ -55,8 +55,8 @@ describe("OidcTokenRefresher", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
|
fetchMock.get(`${config.issuer}.well-known/openid-configuration`, config);
|
||||||
fetchMock.get(`${config.metadata.issuer}jwks`, {
|
fetchMock.get(`${config.issuer}jwks`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
keys: [],
|
keys: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock.post(config.tokenEndpoint, {
|
fetchMock.post(config.token_endpoint, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -81,7 +81,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
it("throws when oidc client cannot be initialised", async () => {
|
it("throws when oidc client cannot be initialised", async () => {
|
||||||
jest.spyOn(logger, "error");
|
jest.spyOn(logger, "error");
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
`${config.metadata.issuer}.well-known/openid-configuration`,
|
`${config.issuer}.well-known/openid-configuration`,
|
||||||
{
|
{
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 404,
|
status: 404,
|
||||||
@ -126,7 +126,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
|
|
||||||
const result = await refresher.doRefreshAccessToken("refresh-token");
|
const result = await refresher.doRefreshAccessToken("refresh-token");
|
||||||
|
|
||||||
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
|
expect(fetchMock).toHaveFetched(config.token_endpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
it("should only have one inflight refresh request at once", async () => {
|
it("should only have one inflight refresh request at once", async () => {
|
||||||
fetchMock
|
fetchMock
|
||||||
.postOnce(
|
.postOnce(
|
||||||
config.tokenEndpoint,
|
config.token_endpoint,
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
{ overwriteRoutes: true },
|
{ overwriteRoutes: true },
|
||||||
)
|
)
|
||||||
.postOnce(
|
.postOnce(
|
||||||
config.tokenEndpoint,
|
config.token_endpoint,
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
const result2 = await first;
|
const result2 = await first;
|
||||||
|
|
||||||
// only one call to token endpoint
|
// only one call to token endpoint
|
||||||
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
|
expect(fetchMock).toHaveFetchedTimes(1, config.token_endpoint);
|
||||||
expect(result1).toEqual({
|
expect(result1).toEqual({
|
||||||
accessToken: "first-new-access-token",
|
accessToken: "first-new-access-token",
|
||||||
refreshToken: "first-new-refresh-token",
|
refreshToken: "first-new-refresh-token",
|
||||||
@ -208,7 +208,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
|
|
||||||
it("should log and rethrow when token refresh fails", async () => {
|
it("should log and rethrow when token refresh fails", async () => {
|
||||||
fetchMock.post(
|
fetchMock.post(
|
||||||
config.tokenEndpoint,
|
config.token_endpoint,
|
||||||
{
|
{
|
||||||
status: 503,
|
status: 503,
|
||||||
headers: {
|
headers: {
|
||||||
@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
// make sure inflight request is cleared after a failure
|
// make sure inflight request is cleared after a failure
|
||||||
fetchMock
|
fetchMock
|
||||||
.postOnce(
|
.postOnce(
|
||||||
config.tokenEndpoint,
|
config.token_endpoint,
|
||||||
{
|
{
|
||||||
status: 503,
|
status: 503,
|
||||||
headers: {
|
headers: {
|
||||||
@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
{ overwriteRoutes: true },
|
{ overwriteRoutes: true },
|
||||||
)
|
)
|
||||||
.postOnce(
|
.postOnce(
|
||||||
config.tokenEndpoint,
|
config.token_endpoint,
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -18,17 +18,18 @@ import { mocked } from "jest-mock";
|
|||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
import { logger } from "../../../src/logger";
|
import { logger } from "../../../src/logger";
|
||||||
import { validateIdToken, validateOIDCIssuerWellKnown } from "../../../src/oidc/validate";
|
import { ValidatedAuthMetadata, validateIdToken, validateAuthMetadata } from "../../../src/oidc/validate";
|
||||||
import { OidcError } from "../../../src/oidc/error";
|
import { OidcError } from "../../../src/oidc/error";
|
||||||
|
|
||||||
jest.mock("jwt-decode");
|
jest.mock("jwt-decode");
|
||||||
|
|
||||||
describe("validateOIDCIssuerWellKnown", () => {
|
describe("validateOIDCIssuerWellKnown", () => {
|
||||||
const validWk: any = {
|
const validWk: ValidatedAuthMetadata = {
|
||||||
|
issuer: "https://test.org",
|
||||||
authorization_endpoint: "https://test.org/authorize",
|
authorization_endpoint: "https://test.org/authorize",
|
||||||
token_endpoint: "https://authorize.org/token",
|
token_endpoint: "https://authorize.org/token",
|
||||||
registration_endpoint: "https://authorize.org/regsiter",
|
registration_endpoint: "https://authorize.org/register",
|
||||||
revocation_endpoint: "https://authorize.org/regsiter",
|
revocation_endpoint: "https://authorize.org/revoke",
|
||||||
response_types_supported: ["code"],
|
response_types_supported: ["code"],
|
||||||
grant_types_supported: ["authorization_code"],
|
grant_types_supported: ["authorization_code"],
|
||||||
code_challenge_methods_supported: ["S256"],
|
code_challenge_methods_supported: ["S256"],
|
||||||
@ -44,14 +45,14 @@ describe("validateOIDCIssuerWellKnown", () => {
|
|||||||
|
|
||||||
it("should throw OP support error when wellKnown is not an object", () => {
|
it("should throw OP support error when wellKnown is not an object", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
validateOIDCIssuerWellKnown([]);
|
validateAuthMetadata([]);
|
||||||
}).toThrow(OidcError.OpSupport);
|
}).toThrow(OidcError.OpSupport);
|
||||||
expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed");
|
expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log all errors before throwing", () => {
|
it("should log all errors before throwing", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
validateOIDCIssuerWellKnown({
|
validateAuthMetadata({
|
||||||
...validWk,
|
...validWk,
|
||||||
authorization_endpoint: undefined,
|
authorization_endpoint: undefined,
|
||||||
response_types_supported: [],
|
response_types_supported: [],
|
||||||
@ -62,24 +63,31 @@ describe("validateOIDCIssuerWellKnown", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return validated issuer config", () => {
|
it("should return validated issuer config", () => {
|
||||||
expect(validateOIDCIssuerWellKnown(validWk)).toEqual({
|
expect(validateAuthMetadata(validWk)).toEqual(
|
||||||
authorizationEndpoint: validWk.authorization_endpoint,
|
expect.objectContaining({
|
||||||
tokenEndpoint: validWk.token_endpoint,
|
issuer: validWk.issuer,
|
||||||
registrationEndpoint: validWk.registration_endpoint,
|
authorization_endpoint: validWk.authorization_endpoint,
|
||||||
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
|
token_endpoint: validWk.token_endpoint,
|
||||||
accountManagementEndpoint: "https://authorize.org/account",
|
registration_endpoint: validWk.registration_endpoint,
|
||||||
});
|
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
|
||||||
|
account_management_uri: "https://authorize.org/account",
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return validated issuer config without registrationendpoint", () => {
|
it("should return validated issuer config without registration_endpoint", () => {
|
||||||
const wk = { ...validWk };
|
const { registration_endpoint: _, ...wk } = validWk;
|
||||||
delete wk.registration_endpoint;
|
expect(validateAuthMetadata(wk)).toEqual({
|
||||||
expect(validateOIDCIssuerWellKnown(wk)).toEqual({
|
issuer: validWk.issuer,
|
||||||
authorizationEndpoint: validWk.authorization_endpoint,
|
authorization_endpoint: validWk.authorization_endpoint,
|
||||||
tokenEndpoint: validWk.token_endpoint,
|
token_endpoint: validWk.token_endpoint,
|
||||||
registrationEndpoint: undefined,
|
revocation_endpoint: validWk.revocation_endpoint,
|
||||||
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
|
registration_endpoint: undefined,
|
||||||
accountManagementEndpoint: "https://authorize.org/account",
|
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
|
||||||
|
account_management_uri: "https://authorize.org/account",
|
||||||
|
code_challenge_methods_supported: ["S256"],
|
||||||
|
grant_types_supported: ["authorization_code"],
|
||||||
|
response_types_supported: ["code"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -106,7 +114,7 @@ describe("validateOIDCIssuerWellKnown", () => {
|
|||||||
...validWk,
|
...validWk,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
};
|
};
|
||||||
expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcError.OpSupport);
|
expect(() => validateAuthMetadata(wk)).toThrow(OidcError.OpSupport);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -247,6 +247,7 @@ import { ImageInfo } from "./@types/media.ts";
|
|||||||
import { Capabilities, ServerCapabilities } from "./serverCapabilities.ts";
|
import { Capabilities, ServerCapabilities } from "./serverCapabilities.ts";
|
||||||
import { sha256 } from "./digest.ts";
|
import { sha256 } from "./digest.ts";
|
||||||
import { keyFromAuthData } from "./common-crypto/key-passphrase.ts";
|
import { keyFromAuthData } from "./common-crypto/key-passphrase.ts";
|
||||||
|
import { discoverAndValidateOIDCIssuerWellKnown, OidcClientConfig, validateAuthMetadataAndKeys } from "./oidc/index.ts";
|
||||||
|
|
||||||
export type Store = IStore;
|
export type Store = IStore;
|
||||||
|
|
||||||
@ -10352,6 +10353,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @returns Resolves: A promise of an object containing the OIDC issuer if configured
|
* @returns Resolves: A promise of an object containing the OIDC issuer if configured
|
||||||
* @returns Rejects: when the request fails (module:http-api.MatrixError)
|
* @returns Rejects: when the request fails (module:http-api.MatrixError)
|
||||||
* @experimental - part of MSC2965
|
* @experimental - part of MSC2965
|
||||||
|
* @deprecated in favour of getAuthMetadata
|
||||||
*/
|
*/
|
||||||
public async getAuthIssuer(): Promise<{
|
public async getAuthIssuer(): Promise<{
|
||||||
issuer: string;
|
issuer: string;
|
||||||
@ -10360,6 +10362,34 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
|
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover and validate delegated auth configuration
|
||||||
|
* - delegated auth issuer openid-configuration is reachable
|
||||||
|
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||||
|
* Fetches /auth_metadata falling back to legacy implementation using /auth_issuer followed by
|
||||||
|
* https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein.
|
||||||
|
* When successful, validated metadata is returned
|
||||||
|
* @returns validated authentication metadata and optionally signing keys
|
||||||
|
* @throws when delegated auth config is invalid or unreachable
|
||||||
|
* @experimental - part of MSC2965
|
||||||
|
*/
|
||||||
|
public async getAuthMetadata(): Promise<OidcClientConfig> {
|
||||||
|
let authMetadata: unknown | undefined;
|
||||||
|
try {
|
||||||
|
authMetadata = await this.http.request<unknown>(Method.Get, "/auth_metadata", undefined, undefined, {
|
||||||
|
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
|
||||||
|
const { issuer } = await this.getAuthIssuer();
|
||||||
|
return discoverAndValidateOIDCIssuerWellKnown(issuer);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateAuthMetadataAndKeys(authMetadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUnstableDelayQueryOpts(delayOpts: SendDelayedEventRequestOpts): QueryDict {
|
function getUnstableDelayQueryOpts(delayOpts: SendDelayedEventRequestOpts): QueryDict {
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
BearerTokenResponse,
|
BearerTokenResponse,
|
||||||
UserState,
|
UserState,
|
||||||
validateBearerTokenResponse,
|
validateBearerTokenResponse,
|
||||||
ValidatedIssuerMetadata,
|
ValidatedAuthMetadata,
|
||||||
validateIdToken,
|
validateIdToken,
|
||||||
validateStoredUserState,
|
validateStoredUserState,
|
||||||
} from "./validate.ts";
|
} from "./validate.ts";
|
||||||
@ -138,7 +138,7 @@ export const generateOidcAuthorizationUrl = async ({
|
|||||||
urlState,
|
urlState,
|
||||||
}: {
|
}: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
metadata: ValidatedIssuerMetadata;
|
metadata: ValidatedAuthMetadata;
|
||||||
homeserverUrl: string;
|
homeserverUrl: string;
|
||||||
identityServerUrl?: string;
|
identityServerUrl?: string;
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { MetadataService, OidcClientSettingsStore } from "oidc-client-ts";
|
import { MetadataService, OidcClientSettingsStore } from "oidc-client-ts";
|
||||||
|
|
||||||
import { isValidatedIssuerMetadata, validateOIDCIssuerWellKnown } from "./validate.ts";
|
import { validateAuthMetadata } from "./validate.ts";
|
||||||
import { Method, timeoutSignal } from "../http-api/index.ts";
|
import { Method, timeoutSignal } from "../http-api/index.ts";
|
||||||
import { OidcClientConfig } from "./index.ts";
|
import { OidcClientConfig } from "./index.ts";
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ import { OidcClientConfig } from "./index.ts";
|
|||||||
* @param issuer - the OIDC issuer as returned by the /auth_issuer API
|
* @param issuer - the OIDC issuer as returned by the /auth_issuer API
|
||||||
* @returns validated authentication metadata and optionally signing keys
|
* @returns validated authentication metadata and optionally signing keys
|
||||||
* @throws when delegated auth config is invalid or unreachable
|
* @throws when delegated auth config is invalid or unreachable
|
||||||
|
* @deprecated in favour of {@link MatrixClient#getAuthMetadata}
|
||||||
*/
|
*/
|
||||||
export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Promise<OidcClientConfig> => {
|
export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Promise<OidcClientConfig> => {
|
||||||
const issuerOpenIdConfigUrl = new URL(".well-known/openid-configuration", issuer);
|
const issuerOpenIdConfigUrl = new URL(".well-known/openid-configuration", issuer);
|
||||||
@ -38,23 +39,29 @@ export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Pr
|
|||||||
signal: timeoutSignal(5000),
|
signal: timeoutSignal(5000),
|
||||||
});
|
});
|
||||||
const issuerWellKnown = await issuerWellKnownResponse.json();
|
const issuerWellKnown = await issuerWellKnownResponse.json();
|
||||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown);
|
return validateAuthMetadataAndKeys(issuerWellKnown);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
* Validate the authentication metadata and fetch the signing keys from the jwks_uri in the metadata
|
||||||
|
* @param authMetadata - the authentication metadata to validate
|
||||||
|
* @returns validated authentication metadata and signing keys
|
||||||
|
*/
|
||||||
|
export const validateAuthMetadataAndKeys = async (authMetadata: unknown): Promise<OidcClientConfig> => {
|
||||||
|
const validatedIssuerConfig = validateAuthMetadata(authMetadata);
|
||||||
|
|
||||||
// create a temporary settings store, so we can use metadata service for discovery
|
// create a temporary settings store, so we can use metadata service for discovery
|
||||||
const settings = new OidcClientSettingsStore({
|
const settings = new OidcClientSettingsStore({
|
||||||
authority: issuer,
|
authority: validatedIssuerConfig.issuer,
|
||||||
|
metadata: validatedIssuerConfig,
|
||||||
redirect_uri: "", // Not known yet, this is here to make the type checker happy
|
redirect_uri: "", // Not known yet, this is here to make the type checker happy
|
||||||
client_id: "", // Not known yet, this is here to make the type checker happy
|
client_id: "", // Not known yet, this is here to make the type checker happy
|
||||||
});
|
});
|
||||||
const metadataService = new MetadataService(settings);
|
const metadataService = new MetadataService(settings);
|
||||||
const metadata = await metadataService.getMetadata();
|
|
||||||
const signingKeys = (await metadataService.getSigningKeys()) ?? undefined;
|
|
||||||
|
|
||||||
isValidatedIssuerMetadata(metadata);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...validatedIssuerConfig,
|
...validatedIssuerConfig,
|
||||||
metadata,
|
signingKeys: await metadataService.getSigningKeys(),
|
||||||
signingKeys,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SigningKey } from "oidc-client-ts";
|
import type { SigningKey } from "oidc-client-ts";
|
||||||
import { ValidatedIssuerConfig, ValidatedIssuerMetadata } from "./validate.ts";
|
import { ValidatedAuthMetadata } from "./validate.ts";
|
||||||
|
|
||||||
export * from "./authorize.ts";
|
export * from "./authorize.ts";
|
||||||
export * from "./discovery.ts";
|
export * from "./discovery.ts";
|
||||||
@ -28,7 +28,6 @@ export * from "./validate.ts";
|
|||||||
* Validated config for native OIDC authentication, as returned by {@link discoverAndValidateOIDCIssuerWellKnown}.
|
* Validated config for native OIDC authentication, as returned by {@link discoverAndValidateOIDCIssuerWellKnown}.
|
||||||
* Contains metadata and signing keys from the issuer's well-known (https://oidc-issuer.example.com/.well-known/openid-configuration).
|
* Contains metadata and signing keys from the issuer's well-known (https://oidc-issuer.example.com/.well-known/openid-configuration).
|
||||||
*/
|
*/
|
||||||
export interface OidcClientConfig extends ValidatedIssuerConfig {
|
export interface OidcClientConfig extends ValidatedAuthMetadata {
|
||||||
metadata: ValidatedIssuerMetadata;
|
signingKeys: SigningKey[] | null;
|
||||||
signingKeys?: SigningKey[];
|
|
||||||
}
|
}
|
||||||
|
@ -65,12 +65,12 @@ export const registerOidcClient = async (
|
|||||||
delegatedAuthConfig: OidcClientConfig,
|
delegatedAuthConfig: OidcClientConfig,
|
||||||
clientMetadata: OidcRegistrationClientMetadata,
|
clientMetadata: OidcRegistrationClientMetadata,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
if (!delegatedAuthConfig.registrationEndpoint) {
|
if (!delegatedAuthConfig.registration_endpoint) {
|
||||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||||
}
|
}
|
||||||
|
|
||||||
const grantTypes: NonEmptyArray<string> = ["authorization_code", "refresh_token"];
|
const grantTypes: NonEmptyArray<string> = ["authorization_code", "refresh_token"];
|
||||||
if (grantTypes.some((scope) => !delegatedAuthConfig.metadata.grant_types_supported.includes(scope))) {
|
if (grantTypes.some((scope) => !delegatedAuthConfig.grant_types_supported.includes(scope))) {
|
||||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ export const registerOidcClient = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(delegatedAuthConfig.registrationEndpoint, {
|
const response = await fetch(delegatedAuthConfig.registration_endpoint, {
|
||||||
method: Method.Post,
|
method: Method.Post,
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(metadata),
|
body: JSON.stringify(metadata),
|
||||||
|
@ -77,11 +77,12 @@ export class OidcTokenRefresher {
|
|||||||
const scope = generateScope(deviceId);
|
const scope = generateScope(deviceId);
|
||||||
|
|
||||||
this.oidcClient = new OidcClient({
|
this.oidcClient = new OidcClient({
|
||||||
...config.metadata,
|
metadata: config,
|
||||||
|
signingKeys: config.signingKeys ?? undefined,
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
scope,
|
scope,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
authority: config.metadata.issuer,
|
authority: config.issuer,
|
||||||
stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }),
|
stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -20,13 +20,28 @@ import { IdTokenClaims, OidcMetadata, SigninResponse } from "oidc-client-ts";
|
|||||||
import { logger } from "../logger.ts";
|
import { logger } from "../logger.ts";
|
||||||
import { OidcError } from "./error.ts";
|
import { OidcError } from "./error.ts";
|
||||||
|
|
||||||
export type ValidatedIssuerConfig = {
|
/**
|
||||||
authorizationEndpoint: string;
|
* Metadata from OIDC authority discovery
|
||||||
tokenEndpoint: string;
|
* With validated properties required in type
|
||||||
registrationEndpoint?: string;
|
*/
|
||||||
accountManagementEndpoint?: string;
|
export type ValidatedAuthMetadata = Partial<OidcMetadata> &
|
||||||
accountManagementActionsSupported?: string[];
|
Pick<
|
||||||
};
|
OidcMetadata,
|
||||||
|
| "issuer"
|
||||||
|
| "authorization_endpoint"
|
||||||
|
| "token_endpoint"
|
||||||
|
| "revocation_endpoint"
|
||||||
|
| "response_types_supported"
|
||||||
|
| "grant_types_supported"
|
||||||
|
| "code_challenge_methods_supported"
|
||||||
|
> & {
|
||||||
|
// MSC2965 extensions to the OIDC spec
|
||||||
|
account_management_uri?: string;
|
||||||
|
account_management_actions_supported?: string[];
|
||||||
|
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported`
|
||||||
|
// even though it is part of the OIDC spec
|
||||||
|
prompt_values_supported?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
!!value && typeof value === "object" && !Array.isArray(value);
|
!!value && typeof value === "object" && !Array.isArray(value);
|
||||||
@ -67,78 +82,39 @@ const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, val
|
|||||||
* Validates issuer `.well-known/openid-configuration`
|
* Validates issuer `.well-known/openid-configuration`
|
||||||
* As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html
|
* As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||||
* validates that OP is compatible with Element's OIDC flow
|
* validates that OP is compatible with Element's OIDC flow
|
||||||
* @param wellKnown - json object
|
* @param authMetadata - json object
|
||||||
* @returns valid issuer config
|
* @returns valid issuer config
|
||||||
* @throws Error - when issuer config is not found or is invalid
|
* @throws Error - when issuer config is not found or is invalid
|
||||||
*/
|
*/
|
||||||
export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuerConfig => {
|
export const validateAuthMetadata = (authMetadata: unknown): ValidatedAuthMetadata => {
|
||||||
if (!isRecord(wellKnown)) {
|
if (!isRecord(authMetadata)) {
|
||||||
logger.error("Issuer configuration not found or malformed");
|
logger.error("Issuer configuration not found or malformed");
|
||||||
throw new Error(OidcError.OpSupport);
|
throw new Error(OidcError.OpSupport);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInvalid = [
|
const isInvalid = [
|
||||||
requiredStringProperty(wellKnown, "authorization_endpoint"),
|
requiredStringProperty(authMetadata, "issuer"),
|
||||||
requiredStringProperty(wellKnown, "token_endpoint"),
|
requiredStringProperty(authMetadata, "authorization_endpoint"),
|
||||||
requiredStringProperty(wellKnown, "revocation_endpoint"),
|
requiredStringProperty(authMetadata, "token_endpoint"),
|
||||||
optionalStringProperty(wellKnown, "registration_endpoint"),
|
requiredStringProperty(authMetadata, "revocation_endpoint"),
|
||||||
optionalStringProperty(wellKnown, "account_management_uri"),
|
optionalStringProperty(authMetadata, "registration_endpoint"),
|
||||||
optionalStringProperty(wellKnown, "device_authorization_endpoint"),
|
optionalStringProperty(authMetadata, "account_management_uri"),
|
||||||
optionalStringArrayProperty(wellKnown, "account_management_actions_supported"),
|
optionalStringProperty(authMetadata, "device_authorization_endpoint"),
|
||||||
requiredArrayValue(wellKnown, "response_types_supported", "code"),
|
optionalStringArrayProperty(authMetadata, "account_management_actions_supported"),
|
||||||
requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"),
|
requiredArrayValue(authMetadata, "response_types_supported", "code"),
|
||||||
requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"),
|
requiredArrayValue(authMetadata, "grant_types_supported", "authorization_code"),
|
||||||
|
requiredArrayValue(authMetadata, "code_challenge_methods_supported", "S256"),
|
||||||
|
optionalStringArrayProperty(authMetadata, "prompt_values_supported"),
|
||||||
].some((isValid) => !isValid);
|
].some((isValid) => !isValid);
|
||||||
|
|
||||||
if (!isInvalid) {
|
if (!isInvalid) {
|
||||||
return {
|
return authMetadata as ValidatedAuthMetadata;
|
||||||
authorizationEndpoint: <string>wellKnown["authorization_endpoint"],
|
|
||||||
tokenEndpoint: <string>wellKnown["token_endpoint"],
|
|
||||||
registrationEndpoint: <string>wellKnown["registration_endpoint"],
|
|
||||||
accountManagementEndpoint: <string>wellKnown["account_management_uri"],
|
|
||||||
accountManagementActionsSupported: <string[]>wellKnown["account_management_actions_supported"],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error("Issuer configuration not valid");
|
logger.error("Issuer configuration not valid");
|
||||||
throw new Error(OidcError.OpSupport);
|
throw new Error(OidcError.OpSupport);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadata from OIDC authority discovery
|
|
||||||
* With validated properties required in type
|
|
||||||
*/
|
|
||||||
export type ValidatedIssuerMetadata = Partial<OidcMetadata> &
|
|
||||||
Pick<
|
|
||||||
OidcMetadata,
|
|
||||||
| "issuer"
|
|
||||||
| "authorization_endpoint"
|
|
||||||
| "token_endpoint"
|
|
||||||
| "registration_endpoint"
|
|
||||||
| "revocation_endpoint"
|
|
||||||
| "response_types_supported"
|
|
||||||
| "grant_types_supported"
|
|
||||||
| "code_challenge_methods_supported"
|
|
||||||
| "device_authorization_endpoint"
|
|
||||||
> & {
|
|
||||||
// MSC2965 extensions to the OIDC spec
|
|
||||||
account_management_uri?: string;
|
|
||||||
account_management_actions_supported?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps validateOIDCIssuerWellKnown in a type assertion
|
|
||||||
* that asserts expected properties are present
|
|
||||||
* (Typescript assertions cannot be arrow functions)
|
|
||||||
* @param metadata - issuer openid-configuration response
|
|
||||||
* @throws when metadata validation fails
|
|
||||||
*/
|
|
||||||
export function isValidatedIssuerMetadata(
|
|
||||||
metadata: Partial<OidcMetadata>,
|
|
||||||
): asserts metadata is ValidatedIssuerMetadata {
|
|
||||||
validateOIDCIssuerWellKnown(metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const decodeIdToken = (token: string): IdTokenClaims => {
|
export const decodeIdToken = (token: string): IdTokenClaims => {
|
||||||
try {
|
try {
|
||||||
return jwtDecode<IdTokenClaims>(token);
|
return jwtDecode<IdTokenClaims>(token);
|
||||||
|
@ -27,7 +27,7 @@ import { logger } from "../logger.ts";
|
|||||||
import { MSC4108SecureChannel } from "./channels/MSC4108SecureChannel.ts";
|
import { MSC4108SecureChannel } from "./channels/MSC4108SecureChannel.ts";
|
||||||
import { MatrixError } from "../http-api/index.ts";
|
import { MatrixError } from "../http-api/index.ts";
|
||||||
import { sleep } from "../utils.ts";
|
import { sleep } from "../utils.ts";
|
||||||
import { DEVICE_CODE_SCOPE, discoverAndValidateOIDCIssuerWellKnown, OidcClientConfig } from "../oidc/index.ts";
|
import { DEVICE_CODE_SCOPE, OidcClientConfig } from "../oidc/index.ts";
|
||||||
import { CryptoApi } from "../crypto-api/index.ts";
|
import { CryptoApi } from "../crypto-api/index.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -189,13 +189,12 @@ export class MSC4108SignInWithQR {
|
|||||||
// MSC4108-Flow: NewScanned -send protocols message
|
// MSC4108-Flow: NewScanned -send protocols message
|
||||||
let oidcClientConfig: OidcClientConfig | undefined;
|
let oidcClientConfig: OidcClientConfig | undefined;
|
||||||
try {
|
try {
|
||||||
const { issuer } = await this.client!.getAuthIssuer();
|
oidcClientConfig = await this.client!.getAuthMetadata();
|
||||||
oidcClientConfig = await discoverAndValidateOIDCIssuerWellKnown(issuer);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Failed to discover OIDC metadata", e);
|
logger.error("Failed to discover OIDC metadata", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oidcClientConfig?.metadata.grant_types_supported.includes(DEVICE_CODE_SCOPE)) {
|
if (oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE)) {
|
||||||
await this.send<ProtocolsPayload>({
|
await this.send<ProtocolsPayload>({
|
||||||
type: PayloadType.Protocols,
|
type: PayloadType.Protocols,
|
||||||
protocols: ["device_authorization_grant"],
|
protocols: ["device_authorization_grant"],
|
||||||
|
@ -27,6 +27,7 @@ import { RoomMember } from "./models/room-member.ts";
|
|||||||
import { EventType } from "./@types/event.ts";
|
import { EventType } from "./@types/event.ts";
|
||||||
import { DecryptionFailureCode } from "./crypto-api/index.ts";
|
import { DecryptionFailureCode } from "./crypto-api/index.ts";
|
||||||
import { DecryptionError, EventDecryptionResult } from "./common-crypto/CryptoBackend.ts";
|
import { DecryptionError, EventDecryptionResult } from "./common-crypto/CryptoBackend.ts";
|
||||||
|
import { OidcClientConfig, ValidatedAuthMetadata } from "./oidc/index.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a {@link MatrixEvent}.
|
* Create a {@link MatrixEvent}.
|
||||||
@ -188,3 +189,45 @@ export async function decryptExistingEvent(
|
|||||||
} as Parameters<MatrixEvent["attemptDecryption"]>[0];
|
} as Parameters<MatrixEvent["attemptDecryption"]>[0];
|
||||||
await mxEvent.attemptDecryption(mockCrypto);
|
await mxEvent.attemptDecryption(mockCrypto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a valid OidcClientConfig with minimum valid values
|
||||||
|
* @param issuer used as the base for all other urls
|
||||||
|
* @param additionalGrantTypes to add to the default grant types
|
||||||
|
* @returns OidcClientConfig
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export const makeDelegatedAuthConfig = (
|
||||||
|
issuer = "https://auth.org/",
|
||||||
|
additionalGrantTypes: string[] = [],
|
||||||
|
): OidcClientConfig => {
|
||||||
|
const metadata = mockOpenIdConfiguration(issuer, additionalGrantTypes);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...metadata,
|
||||||
|
signingKeys: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Useful for mocking <issuer>/.well-known/openid-configuration
|
||||||
|
* @param issuer used as the base for all other urls
|
||||||
|
* @param additionalGrantTypes to add to the default grant types
|
||||||
|
* @returns ValidatedAuthMetadata
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export const mockOpenIdConfiguration = (
|
||||||
|
issuer = "https://auth.org/",
|
||||||
|
additionalGrantTypes: string[] = [],
|
||||||
|
): ValidatedAuthMetadata => ({
|
||||||
|
issuer,
|
||||||
|
revocation_endpoint: issuer + "revoke",
|
||||||
|
token_endpoint: issuer + "token",
|
||||||
|
authorization_endpoint: issuer + "auth",
|
||||||
|
registration_endpoint: issuer + "registration",
|
||||||
|
device_authorization_endpoint: issuer + "device",
|
||||||
|
jwks_uri: issuer + "jwks",
|
||||||
|
response_types_supported: ["code"],
|
||||||
|
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
|
||||||
|
code_challenge_methods_supported: ["S256"],
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user