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
Update MSC2965 OIDC Discovery implementation (#4064)
This commit is contained in:
committed by
GitHub
parent
be3913e8a5
commit
a26fc46ed4
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OidcClientConfig } from "../../src";
|
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
|
||||||
import { ValidatedIssuerMetadata } from "../../src/oidc/validate";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a valid OidcClientConfig with minimum valid values
|
* Makes a valid OidcClientConfig with minimum valid values
|
||||||
@ -26,8 +25,7 @@ export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClien
|
|||||||
const metadata = mockOpenIdConfiguration(issuer);
|
const metadata = mockOpenIdConfiguration(issuer);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
issuer,
|
accountManagementEndpoint: issuer + "account",
|
||||||
account: issuer + "account",
|
|
||||||
registrationEndpoint: metadata.registration_endpoint,
|
registrationEndpoint: metadata.registration_endpoint,
|
||||||
authorizationEndpoint: metadata.authorization_endpoint,
|
authorizationEndpoint: metadata.authorization_endpoint,
|
||||||
tokenEndpoint: metadata.token_endpoint,
|
tokenEndpoint: metadata.token_endpoint,
|
||||||
|
@ -15,13 +15,10 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
|
||||||
import MockHttpBackend from "matrix-mock-request";
|
import MockHttpBackend from "matrix-mock-request";
|
||||||
|
|
||||||
import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src";
|
import { AutoDiscoveryAction } from "../../src";
|
||||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||||
import { OidcError } from "../../src/oidc/error";
|
|
||||||
import { makeDelegatedAuthConfig } from "../test-utils/oidc";
|
|
||||||
|
|
||||||
// keep to reset the fetch function after using MockHttpBackend
|
// keep to reset the fetch function after using MockHttpBackend
|
||||||
// @ts-ignore private property
|
// @ts-ignore private property
|
||||||
@ -409,10 +406,6 @@ describe("AutoDiscovery", function () {
|
|||||||
error: null,
|
error: null,
|
||||||
base_url: null,
|
base_url: null,
|
||||||
},
|
},
|
||||||
"m.authentication": {
|
|
||||||
state: "IGNORE",
|
|
||||||
error: OidcError.NotSupported,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(conf).toEqual(expected);
|
expect(conf).toEqual(expected);
|
||||||
@ -450,10 +443,6 @@ describe("AutoDiscovery", function () {
|
|||||||
error: null,
|
error: null,
|
||||||
base_url: null,
|
base_url: null,
|
||||||
},
|
},
|
||||||
"m.authentication": {
|
|
||||||
state: "IGNORE",
|
|
||||||
error: OidcError.NotSupported,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(conf).toEqual(expected);
|
expect(conf).toEqual(expected);
|
||||||
@ -476,9 +465,6 @@ describe("AutoDiscovery", function () {
|
|||||||
// Note: we also expect this test to trim the trailing slash
|
// Note: we also expect this test to trim the trailing slash
|
||||||
base_url: "https://chat.example.org/",
|
base_url: "https://chat.example.org/",
|
||||||
},
|
},
|
||||||
"m.authentication": {
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend.flushAllExpected(),
|
||||||
@ -494,10 +480,6 @@ describe("AutoDiscovery", function () {
|
|||||||
error: null,
|
error: null,
|
||||||
base_url: null,
|
base_url: null,
|
||||||
},
|
},
|
||||||
"m.authentication": {
|
|
||||||
state: "FAIL_ERROR",
|
|
||||||
error: OidcError.Misconfigured,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(conf).toEqual(expected);
|
expect(conf).toEqual(expected);
|
||||||
@ -728,10 +710,6 @@ describe("AutoDiscovery", function () {
|
|||||||
error: null,
|
error: null,
|
||||||
base_url: "https://identity.example.org",
|
base_url: "https://identity.example.org",
|
||||||
},
|
},
|
||||||
"m.authentication": {
|
|
||||||
state: "IGNORE",
|
|
||||||
error: OidcError.NotSupported,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(conf).toEqual(expected);
|
expect(conf).toEqual(expected);
|
||||||
@ -784,10 +762,6 @@ describe("AutoDiscovery", function () {
|
|||||||
"org.example.custom.property": {
|
"org.example.custom.property": {
|
||||||
cupcakes: "yes",
|
cupcakes: "yes",
|
||||||
},
|
},
|
||||||
"m.authentication": {
|
|
||||||
state: "IGNORE",
|
|
||||||
error: OidcError.NotSupported,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(conf).toEqual(expected);
|
expect(conf).toEqual(expected);
|
||||||
@ -897,75 +871,4 @@ describe("AutoDiscovery", function () {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("m.authentication", () => {
|
|
||||||
const homeserverName = "example.org";
|
|
||||||
const homeserverUrl = "https://chat.example.org/";
|
|
||||||
const issuer = "https://auth.org/";
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
// make these tests independent from fetch mocking above
|
|
||||||
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fetchMock.resetBehavior();
|
|
||||||
fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.5"] });
|
|
||||||
|
|
||||||
fetchMock.get("https://example.org/.well-known/matrix/client", {
|
|
||||||
"m.homeserver": {
|
|
||||||
// Note: we also expect this test to trim the trailing slash
|
|
||||||
base_url: "https://chat.example.org/",
|
|
||||||
},
|
|
||||||
"m.authentication": {
|
|
||||||
issuer,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return valid authentication configuration", async () => {
|
|
||||||
const config = makeDelegatedAuthConfig(issuer);
|
|
||||||
|
|
||||||
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
|
|
||||||
fetchMock.get(`${config.metadata.issuer}jwks`, {
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
keys: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await AutoDiscovery.findClientConfig(homeserverName);
|
|
||||||
|
|
||||||
expect(result[M_AUTHENTICATION.stable!]).toEqual({
|
|
||||||
state: AutoDiscovery.SUCCESS,
|
|
||||||
...config,
|
|
||||||
signingKeys: [],
|
|
||||||
account: undefined,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set state to error for invalid authentication configuration", async () => {
|
|
||||||
const config = makeDelegatedAuthConfig(issuer);
|
|
||||||
// authorization_code is required
|
|
||||||
config.metadata.grant_types_supported = ["openid"];
|
|
||||||
|
|
||||||
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
|
|
||||||
fetchMock.get(`${config.metadata.issuer}jwks`, {
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
keys: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await AutoDiscovery.findClientConfig(homeserverName);
|
|
||||||
|
|
||||||
expect(result[M_AUTHENTICATION.stable!]).toEqual({
|
|
||||||
state: AutoDiscovery.FAIL_ERROR,
|
|
||||||
error: OidcError.OpSupport,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -46,8 +46,8 @@ const realSubtleCrypto = crypto.subtleCrypto;
|
|||||||
|
|
||||||
describe("oidc authorization", () => {
|
describe("oidc authorization", () => {
|
||||||
const delegatedAuthConfig = makeDelegatedAuthConfig();
|
const delegatedAuthConfig = makeDelegatedAuthConfig();
|
||||||
const authorizationEndpoint = delegatedAuthConfig.metadata.authorization_endpoint;
|
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
|
||||||
const tokenEndpoint = delegatedAuthConfig.metadata.token_endpoint;
|
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
|
||||||
const clientId = "xyz789";
|
const clientId = "xyz789";
|
||||||
const baseUrl = "https://test.com";
|
const baseUrl = "https://test.com";
|
||||||
|
|
||||||
@ -58,7 +58,10 @@ describe("oidc authorization", () => {
|
|||||||
jest.spyOn(logger, "warn");
|
jest.spyOn(logger, "warn");
|
||||||
jest.setSystemTime(now);
|
jest.setSystemTime(now);
|
||||||
|
|
||||||
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
|
fetchMock.get(
|
||||||
|
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
|
||||||
|
mockOpenIdConfiguration(),
|
||||||
|
);
|
||||||
|
|
||||||
Object.defineProperty(window, "crypto", {
|
Object.defineProperty(window, "crypto", {
|
||||||
value: {
|
value: {
|
||||||
|
@ -18,10 +18,10 @@ import fetchMockJest from "fetch-mock-jest";
|
|||||||
|
|
||||||
import { OidcError } from "../../../src/oidc/error";
|
import { OidcError } from "../../../src/oidc/error";
|
||||||
import { OidcRegistrationClientMetadata, registerOidcClient } from "../../../src/oidc/register";
|
import { OidcRegistrationClientMetadata, registerOidcClient } from "../../../src/oidc/register";
|
||||||
|
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||||
|
|
||||||
describe("registerOidcClient()", () => {
|
describe("registerOidcClient()", () => {
|
||||||
const issuer = "https://auth.com/";
|
const issuer = "https://auth.com/";
|
||||||
const registrationEndpoint = "https://auth.com/register";
|
|
||||||
const clientName = "Element";
|
const clientName = "Element";
|
||||||
const baseUrl = "https://just.testing";
|
const baseUrl = "https://just.testing";
|
||||||
const metadata: OidcRegistrationClientMetadata = {
|
const metadata: OidcRegistrationClientMetadata = {
|
||||||
@ -35,25 +35,20 @@ describe("registerOidcClient()", () => {
|
|||||||
};
|
};
|
||||||
const dynamicClientId = "xyz789";
|
const dynamicClientId = "xyz789";
|
||||||
|
|
||||||
const delegatedAuthConfig = {
|
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
|
||||||
issuer,
|
|
||||||
registrationEndpoint,
|
|
||||||
authorizationEndpoint: issuer + "auth",
|
|
||||||
tokenEndpoint: issuer + "token",
|
|
||||||
};
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMockJest.mockClear();
|
fetchMockJest.mockClear();
|
||||||
fetchMockJest.resetBehavior();
|
fetchMockJest.resetBehavior();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should make correct request to register client", async () => {
|
it("should make correct request to register client", async () => {
|
||||||
fetchMockJest.post(registrationEndpoint, {
|
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||||
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(
|
||||||
registrationEndpoint,
|
delegatedAuthConfig.registrationEndpoint!,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
@ -77,7 +72,7 @@ describe("registerOidcClient()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should throw when registration request fails", async () => {
|
it("should throw when registration request fails", async () => {
|
||||||
fetchMockJest.post(registrationEndpoint, {
|
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||||
status: 500,
|
status: 500,
|
||||||
});
|
});
|
||||||
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
|
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
|
||||||
@ -86,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(registrationEndpoint, {
|
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||||
status: 200,
|
status: 200,
|
||||||
// no clientId in response
|
// no clientId in response
|
||||||
body: "{}",
|
body: "{}",
|
||||||
|
@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
keys: [],
|
keys: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock.post(config.metadata.token_endpoint, {
|
fetchMock.post(config.tokenEndpoint, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -88,7 +88,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
},
|
},
|
||||||
{ overwriteRoutes: true },
|
{ overwriteRoutes: true },
|
||||||
);
|
);
|
||||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||||
await expect(refresher.oidcClientReady).rejects.toThrow();
|
await expect(refresher.oidcClientReady).rejects.toThrow();
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
"Failed to initialise OIDC client.",
|
"Failed to initialise OIDC client.",
|
||||||
@ -98,7 +98,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("initialises oidc client", async () => {
|
it("initialises oidc client", async () => {
|
||||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||||
await refresher.oidcClientReady;
|
await refresher.oidcClientReady;
|
||||||
|
|
||||||
// @ts-ignore peek at private property to see we initialised the client correctly
|
// @ts-ignore peek at private property to see we initialised the client correctly
|
||||||
@ -114,19 +114,19 @@ describe("OidcTokenRefresher", () => {
|
|||||||
|
|
||||||
describe("doRefreshAccessToken()", () => {
|
describe("doRefreshAccessToken()", () => {
|
||||||
it("should throw when oidcClient has not been initialised", async () => {
|
it("should throw when oidcClient has not been initialised", async () => {
|
||||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||||
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow(
|
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow(
|
||||||
"Cannot get new token before OIDC client is initialised.",
|
"Cannot get new token before OIDC client is initialised.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should refresh the tokens", async () => {
|
it("should refresh the tokens", async () => {
|
||||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||||
await refresher.oidcClientReady;
|
await refresher.oidcClientReady;
|
||||||
|
|
||||||
const result = await refresher.doRefreshAccessToken("refresh-token");
|
const result = await refresher.doRefreshAccessToken("refresh-token");
|
||||||
|
|
||||||
expect(fetchMock).toHaveFetched(config.metadata.token_endpoint, {
|
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should persist the new tokens", async () => {
|
it("should persist the new tokens", async () => {
|
||||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||||
await refresher.oidcClientReady;
|
await refresher.oidcClientReady;
|
||||||
// spy on our stub
|
// spy on our stub
|
||||||
jest.spyOn(refresher, "persistTokens");
|
jest.spyOn(refresher, "persistTokens");
|
||||||
@ -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.metadata.token_endpoint,
|
config.tokenEndpoint,
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
{ overwriteRoutes: true },
|
{ overwriteRoutes: true },
|
||||||
)
|
)
|
||||||
.postOnce(
|
.postOnce(
|
||||||
config.metadata.token_endpoint,
|
config.tokenEndpoint,
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
@ -175,7 +175,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
{ overwriteRoutes: false },
|
{ overwriteRoutes: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||||
await refresher.oidcClientReady;
|
await refresher.oidcClientReady;
|
||||||
// reset call counts
|
// reset call counts
|
||||||
fetchMock.resetHistory();
|
fetchMock.resetHistory();
|
||||||
@ -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.metadata.token_endpoint);
|
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
|
||||||
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.metadata.token_endpoint,
|
config.tokenEndpoint,
|
||||||
{
|
{
|
||||||
status: 503,
|
status: 503,
|
||||||
headers: {
|
headers: {
|
||||||
@ -218,7 +218,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
{ overwriteRoutes: true },
|
{ overwriteRoutes: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||||
await refresher.oidcClientReady;
|
await refresher.oidcClientReady;
|
||||||
|
|
||||||
await expect(refresher.doRefreshAccessToken("refresh-token")).rejects.toThrow();
|
await expect(refresher.doRefreshAccessToken("refresh-token")).rejects.toThrow();
|
||||||
@ -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.metadata.token_endpoint,
|
config.tokenEndpoint,
|
||||||
{
|
{
|
||||||
status: 503,
|
status: 503,
|
||||||
headers: {
|
headers: {
|
||||||
@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
{ overwriteRoutes: true },
|
{ overwriteRoutes: true },
|
||||||
)
|
)
|
||||||
.postOnce(
|
.postOnce(
|
||||||
config.metadata.token_endpoint,
|
config.tokenEndpoint,
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
@ -249,7 +249,7 @@ describe("OidcTokenRefresher", () => {
|
|||||||
{ overwriteRoutes: false },
|
{ overwriteRoutes: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
|
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
|
||||||
await refresher.oidcClientReady;
|
await refresher.oidcClientReady;
|
||||||
// reset call counts
|
// reset call counts
|
||||||
fetchMock.resetHistory();
|
fetchMock.resetHistory();
|
||||||
|
@ -17,105 +17,12 @@ limitations under the License.
|
|||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
import { M_AUTHENTICATION } from "../../../src";
|
|
||||||
import { logger } from "../../../src/logger";
|
import { logger } from "../../../src/logger";
|
||||||
import {
|
import { validateIdToken, validateOIDCIssuerWellKnown } from "../../../src/oidc/validate";
|
||||||
validateIdToken,
|
|
||||||
validateOIDCIssuerWellKnown,
|
|
||||||
validateWellKnownAuthentication,
|
|
||||||
} 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("validateWellKnownAuthentication()", () => {
|
|
||||||
const baseWk = {
|
|
||||||
"m.homeserver": {
|
|
||||||
base_url: "https://hs.org",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
it("should throw not supported error when wellKnown has no m.authentication section", () => {
|
|
||||||
expect(() => validateWellKnownAuthentication(undefined)).toThrow(OidcError.NotSupported);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw misconfigured error when authentication issuer is not a string", () => {
|
|
||||||
const wk = {
|
|
||||||
...baseWk,
|
|
||||||
[M_AUTHENTICATION.stable!]: {
|
|
||||||
issuer: { url: "test.com" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
|
|
||||||
OidcError.Misconfigured,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw misconfigured error when authentication account is not a string", () => {
|
|
||||||
const wk = {
|
|
||||||
...baseWk,
|
|
||||||
[M_AUTHENTICATION.stable!]: {
|
|
||||||
issuer: "test.com",
|
|
||||||
account: { url: "test" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
|
|
||||||
OidcError.Misconfigured,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw misconfigured error when authentication account is false", () => {
|
|
||||||
const wk = {
|
|
||||||
...baseWk,
|
|
||||||
[M_AUTHENTICATION.stable!]: {
|
|
||||||
issuer: "test.com",
|
|
||||||
account: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
|
|
||||||
OidcError.Misconfigured,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return valid config when wk uses stable m.authentication", () => {
|
|
||||||
const wk = {
|
|
||||||
...baseWk,
|
|
||||||
[M_AUTHENTICATION.stable!]: {
|
|
||||||
issuer: "test.com",
|
|
||||||
account: "account.com",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
|
|
||||||
issuer: "test.com",
|
|
||||||
account: "account.com",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return valid config when m.authentication account is missing", () => {
|
|
||||||
const wk = {
|
|
||||||
...baseWk,
|
|
||||||
[M_AUTHENTICATION.stable!]: {
|
|
||||||
issuer: "test.com",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
|
|
||||||
issuer: "test.com",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove unexpected properties", () => {
|
|
||||||
const wk = {
|
|
||||||
...baseWk,
|
|
||||||
[M_AUTHENTICATION.stable!]: {
|
|
||||||
issuer: "test.com",
|
|
||||||
somethingElse: "test",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
|
|
||||||
issuer: "test.com",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("validateOIDCIssuerWellKnown", () => {
|
describe("validateOIDCIssuerWellKnown", () => {
|
||||||
const validWk: any = {
|
const validWk: any = {
|
||||||
authorization_endpoint: "https://test.org/authorize",
|
authorization_endpoint: "https://test.org/authorize",
|
||||||
|
@ -15,19 +15,9 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SigningKey } from "oidc-client-ts";
|
import { IClientWellKnown, IWellKnownConfig, IServerVersions } from "./client";
|
||||||
|
|
||||||
import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, IServerVersions, M_AUTHENTICATION } from "./client";
|
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { MatrixError, Method, timeoutSignal } from "./http-api";
|
import { MatrixError, Method, timeoutSignal } from "./http-api";
|
||||||
import { discoverAndValidateAuthenticationConfig } from "./oidc/discovery";
|
|
||||||
import {
|
|
||||||
ValidatedIssuerConfig,
|
|
||||||
ValidatedIssuerMetadata,
|
|
||||||
validateOIDCIssuerWellKnown,
|
|
||||||
validateWellKnownAuthentication,
|
|
||||||
} from "./oidc/validate";
|
|
||||||
import { OidcError } from "./oidc/error";
|
|
||||||
import { SUPPORTED_MATRIX_VERSIONS } from "./version-support";
|
import { SUPPORTED_MATRIX_VERSIONS } from "./version-support";
|
||||||
|
|
||||||
// Dev note: Auto discovery is part of the spec.
|
// Dev note: Auto discovery is part of the spec.
|
||||||
@ -65,26 +55,9 @@ interface AutoDiscoveryState {
|
|||||||
}
|
}
|
||||||
interface WellKnownConfig extends Omit<IWellKnownConfig, "error">, AutoDiscoveryState {}
|
interface WellKnownConfig extends Omit<IWellKnownConfig, "error">, AutoDiscoveryState {}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated in favour of OidcClientConfig
|
|
||||||
*/
|
|
||||||
interface DelegatedAuthConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig, AutoDiscoveryState {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
export interface OidcClientConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig {
|
|
||||||
metadata: ValidatedIssuerMetadata;
|
|
||||||
signingKeys?: SigningKey[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
||||||
"m.homeserver": WellKnownConfig;
|
"m.homeserver": WellKnownConfig;
|
||||||
"m.identity_server": WellKnownConfig;
|
"m.identity_server": WellKnownConfig;
|
||||||
/**
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
"m.authentication"?: (OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -318,107 +291,10 @@ export class AutoDiscovery {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const authConfig = await this.discoverAndValidateAuthenticationConfig(wellknown);
|
|
||||||
clientConfig[M_AUTHENTICATION.stable!] = authConfig;
|
|
||||||
|
|
||||||
// Step 8: Give the config to the caller (finally)
|
// Step 8: Give the config to the caller (finally)
|
||||||
return Promise.resolve(clientConfig);
|
return Promise.resolve(clientConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate delegated auth configuration
|
|
||||||
* @deprecated use discoverAndValidateAuthenticationConfig
|
|
||||||
* - m.authentication config is present and valid
|
|
||||||
* - delegated auth issuer openid-configuration is reachable
|
|
||||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
|
||||||
* When successful, DelegatedAuthConfig will be returned with endpoints used for delegated auth
|
|
||||||
* Any errors are caught, and AutoDiscoveryState returned with error
|
|
||||||
* @param wellKnown - configuration object as returned
|
|
||||||
* by the .well-known auto-discovery endpoint
|
|
||||||
* @returns Config or failure result
|
|
||||||
*/
|
|
||||||
public static async validateDiscoveryAuthenticationConfig(
|
|
||||||
wellKnown: IClientWellKnown,
|
|
||||||
): Promise<DelegatedAuthConfig | AutoDiscoveryState> {
|
|
||||||
try {
|
|
||||||
const authentication = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown) || undefined;
|
|
||||||
const homeserverAuthenticationConfig = validateWellKnownAuthentication(authentication);
|
|
||||||
|
|
||||||
const issuerOpenIdConfigUrl = `${this.sanitizeWellKnownUrl(
|
|
||||||
homeserverAuthenticationConfig.issuer,
|
|
||||||
)}/.well-known/openid-configuration`;
|
|
||||||
const issuerWellKnown = await this.fetchWellKnownObject<unknown>(issuerOpenIdConfigUrl);
|
|
||||||
|
|
||||||
if (issuerWellKnown.action !== AutoDiscoveryAction.SUCCESS) {
|
|
||||||
logger.error("Failed to fetch issuer openid configuration");
|
|
||||||
throw new Error(OidcError.General);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown.raw);
|
|
||||||
|
|
||||||
const delegatedAuthConfig: DelegatedAuthConfig = {
|
|
||||||
state: AutoDiscoveryAction.SUCCESS,
|
|
||||||
error: null,
|
|
||||||
...homeserverAuthenticationConfig,
|
|
||||||
...validatedIssuerConfig,
|
|
||||||
};
|
|
||||||
return delegatedAuthConfig;
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = (error as Error).message as unknown as OidcError;
|
|
||||||
const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General;
|
|
||||||
|
|
||||||
const state =
|
|
||||||
errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR;
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
error: errorType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate delegated auth configuration
|
|
||||||
* - m.authentication config is present and valid
|
|
||||||
* - delegated auth issuer openid-configuration is reachable
|
|
||||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
|
||||||
* When successful, validated authentication metadata and optionally signing keys will be returned
|
|
||||||
* Any errors are caught, and AutoDiscoveryState returned with error
|
|
||||||
* @param wellKnown - configuration object as returned
|
|
||||||
* by the .well-known auto-discovery endpoint
|
|
||||||
* @returns Config or failure result
|
|
||||||
*/
|
|
||||||
public static async discoverAndValidateAuthenticationConfig(
|
|
||||||
wellKnown: IClientWellKnown,
|
|
||||||
): Promise<(OidcClientConfig & AutoDiscoveryState) | AutoDiscoveryState> {
|
|
||||||
try {
|
|
||||||
const authentication = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown) || undefined;
|
|
||||||
const result = await discoverAndValidateAuthenticationConfig(authentication);
|
|
||||||
|
|
||||||
// include this for backwards compatibility
|
|
||||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(result.metadata);
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
state: AutoDiscoveryAction.SUCCESS,
|
|
||||||
error: null,
|
|
||||||
...validatedIssuerConfig,
|
|
||||||
...result,
|
|
||||||
};
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = (error as Error).message as unknown as OidcError;
|
|
||||||
const errorType = Object.values(OidcError).includes(errorMessage) ? errorMessage : OidcError.General;
|
|
||||||
|
|
||||||
const state =
|
|
||||||
errorType === OidcError.NotSupported ? AutoDiscoveryAction.IGNORE : AutoDiscoveryAction.FAIL_ERROR;
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
error: errorType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to automatically discover client configuration information
|
* Attempts to automatically discover client configuration information
|
||||||
* prior to logging in. Such information includes the homeserver URL
|
* prior to logging in. Such information includes the homeserver URL
|
||||||
|
@ -625,13 +625,10 @@ export interface IServerVersions {
|
|||||||
unstable_features: Record<string, boolean>;
|
unstable_features: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const M_AUTHENTICATION = new UnstableValue("m.authentication", "org.matrix.msc2965.authentication");
|
|
||||||
|
|
||||||
export interface IClientWellKnown {
|
export interface IClientWellKnown {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
"m.homeserver"?: IWellKnownConfig;
|
"m.homeserver"?: IWellKnownConfig;
|
||||||
"m.identity_server"?: IWellKnownConfig;
|
"m.identity_server"?: IWellKnownConfig;
|
||||||
[M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWellKnownConfig<T = IClientWellKnown> {
|
export interface IWellKnownConfig<T = IClientWellKnown> {
|
||||||
@ -645,14 +642,6 @@ export interface IWellKnownConfig<T = IClientWellKnown> {
|
|||||||
server_name?: string;
|
server_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDelegatedAuthConfig {
|
|
||||||
// MSC2965
|
|
||||||
/** The OIDC Provider/issuer the client should use */
|
|
||||||
issuer: string;
|
|
||||||
/** The optional URL of the web UI where the user can manage their account */
|
|
||||||
account?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IKeyBackupPath {
|
interface IKeyBackupPath {
|
||||||
path: string;
|
path: string;
|
||||||
queryData?: {
|
queryData?: {
|
||||||
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||||||
|
|
||||||
import { IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts";
|
import { IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts";
|
||||||
|
|
||||||
import { IDelegatedAuthConfig } from "../client";
|
|
||||||
import { subtleCrypto, TextEncoder } from "../crypto/crypto";
|
import { subtleCrypto, TextEncoder } from "../crypto/crypto";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { randomString } from "../randomstring";
|
import { randomString } from "../randomstring";
|
||||||
@ -209,7 +208,7 @@ export const completeAuthorizationCodeGrant = async (
|
|||||||
code: string,
|
code: string,
|
||||||
state: string,
|
state: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
oidcClientSettings: IDelegatedAuthConfig & { clientId: string };
|
oidcClientSettings: { clientId: string; issuer: string };
|
||||||
tokenResponse: BearerTokenResponse;
|
tokenResponse: BearerTokenResponse;
|
||||||
homeserverUrl: string;
|
homeserverUrl: string;
|
||||||
idTokenClaims: IdTokenClaims;
|
idTokenClaims: IdTokenClaims;
|
||||||
|
@ -14,36 +14,35 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MetadataService, OidcClientSettingsStore, SigningKey } from "oidc-client-ts";
|
import { MetadataService, OidcClientSettingsStore } from "oidc-client-ts";
|
||||||
|
|
||||||
import { IDelegatedAuthConfig } from "../client";
|
import { isValidatedIssuerMetadata, validateOIDCIssuerWellKnown } from "./validate";
|
||||||
import { isValidatedIssuerMetadata, ValidatedIssuerMetadata, validateWellKnownAuthentication } from "./validate";
|
import { Method, timeoutSignal } from "../http-api";
|
||||||
|
import { OidcClientConfig } from "./index";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
* Discover and validate delegated auth configuration
|
* Discover and validate delegated auth configuration
|
||||||
* - m.authentication config is present and valid
|
|
||||||
* - delegated auth issuer openid-configuration is reachable
|
* - delegated auth issuer openid-configuration is reachable
|
||||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||||
|
* Fetches https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein.
|
||||||
* When successful, validated metadata is returned
|
* When successful, validated metadata is returned
|
||||||
* @param wellKnown - configuration object as returned
|
* @param issuer - the OIDC issuer as returned by the /auth_issuer API
|
||||||
* by the .well-known auto-discovery endpoint
|
|
||||||
* @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
|
||||||
*/
|
*/
|
||||||
export const discoverAndValidateAuthenticationConfig = async (
|
export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Promise<OidcClientConfig> => {
|
||||||
authenticationConfig?: IDelegatedAuthConfig,
|
const issuerOpenIdConfigUrl = new URL(".well-known/openid-configuration", issuer);
|
||||||
): Promise<
|
const issuerWellKnownResponse = await fetch(issuerOpenIdConfigUrl, {
|
||||||
IDelegatedAuthConfig & {
|
method: Method.Get,
|
||||||
metadata: ValidatedIssuerMetadata;
|
signal: timeoutSignal(5000),
|
||||||
signingKeys?: SigningKey[];
|
});
|
||||||
}
|
const issuerWellKnown = await issuerWellKnownResponse.json();
|
||||||
> => {
|
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown);
|
||||||
const homeserverAuthenticationConfig = validateWellKnownAuthentication(authenticationConfig);
|
|
||||||
|
|
||||||
// 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: homeserverAuthenticationConfig.issuer,
|
authority: issuer,
|
||||||
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
|
||||||
});
|
});
|
||||||
@ -54,7 +53,7 @@ export const discoverAndValidateAuthenticationConfig = async (
|
|||||||
isValidatedIssuerMetadata(metadata);
|
isValidatedIssuerMetadata(metadata);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...homeserverAuthenticationConfig,
|
...validatedIssuerConfig,
|
||||||
metadata,
|
metadata,
|
||||||
signingKeys,
|
signingKeys,
|
||||||
};
|
};
|
||||||
|
@ -14,9 +14,21 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { SigningKey } from "oidc-client-ts";
|
||||||
|
import { ValidatedIssuerConfig, ValidatedIssuerMetadata } from "./validate";
|
||||||
|
|
||||||
export * from "./authorize";
|
export * from "./authorize";
|
||||||
export * from "./discovery";
|
export * from "./discovery";
|
||||||
export * from "./error";
|
export * from "./error";
|
||||||
export * from "./register";
|
export * from "./register";
|
||||||
export * from "./tokenRefresher";
|
export * from "./tokenRefresher";
|
||||||
export * from "./validate";
|
export * from "./validate";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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).
|
||||||
|
*/
|
||||||
|
export interface OidcClientConfig extends ValidatedIssuerConfig {
|
||||||
|
metadata: ValidatedIssuerMetadata;
|
||||||
|
signingKeys?: SigningKey[];
|
||||||
|
}
|
||||||
|
@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IDelegatedAuthConfig } from "../client";
|
import { OidcClientConfig } from ".";
|
||||||
import { OidcError } from "./error";
|
import { OidcError } from "./error";
|
||||||
import { Method } from "../http-api";
|
import { Method } from "../http-api";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { ValidatedIssuerConfig } from "./validate";
|
|
||||||
import { NonEmptyArray } from "../@types/common";
|
import { NonEmptyArray } from "../@types/common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,13 +111,13 @@ const doRegistration = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts dynamic registration against the configured registration endpoint
|
* Attempts dynamic registration against the configured registration endpoint
|
||||||
* @param delegatedAuthConfig - Auth config from ValidatedServerConfig
|
* @param delegatedAuthConfig - Auth config from {@link discoverAndValidateOIDCIssuerWellKnown}
|
||||||
* @param clientMetadata - The metadata for the client which to register
|
* @param clientMetadata - The metadata for the client which to register
|
||||||
* @returns Promise<string> resolved with registered clientId
|
* @returns Promise<string> resolved with registered clientId
|
||||||
* @throws when registration is not supported, on failed request or invalid response
|
* @throws when registration is not supported, on failed request or invalid response
|
||||||
*/
|
*/
|
||||||
export const registerOidcClient = async (
|
export const registerOidcClient = async (
|
||||||
delegatedAuthConfig: IDelegatedAuthConfig & ValidatedIssuerConfig,
|
delegatedAuthConfig: OidcClientConfig,
|
||||||
clientMetadata: OidcRegistrationClientMetadata,
|
clientMetadata: OidcRegistrationClientMetadata,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
if (!delegatedAuthConfig.registrationEndpoint) {
|
if (!delegatedAuthConfig.registrationEndpoint) {
|
||||||
|
@ -17,9 +17,8 @@ limitations under the License.
|
|||||||
import { IdTokenClaims, OidcClient, WebStorageStateStore } from "oidc-client-ts";
|
import { IdTokenClaims, OidcClient, WebStorageStateStore } from "oidc-client-ts";
|
||||||
|
|
||||||
import { AccessTokens } from "../http-api";
|
import { AccessTokens } from "../http-api";
|
||||||
import { IDelegatedAuthConfig } from "../client";
|
|
||||||
import { generateScope } from "./authorize";
|
import { generateScope } from "./authorize";
|
||||||
import { discoverAndValidateAuthenticationConfig } from "./discovery";
|
import { discoverAndValidateOIDCIssuerWellKnown } from "./discovery";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,9 +41,9 @@ export class OidcTokenRefresher {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
/**
|
/**
|
||||||
* Delegated auth config as found in matrix client .well-known
|
* The OIDC issuer as returned by the /auth_issuer API
|
||||||
*/
|
*/
|
||||||
authConfig: IDelegatedAuthConfig,
|
issuer: string,
|
||||||
/**
|
/**
|
||||||
* id of this client as registered with the OP
|
* id of this client as registered with the OP
|
||||||
*/
|
*/
|
||||||
@ -63,17 +62,17 @@ export class OidcTokenRefresher {
|
|||||||
*/
|
*/
|
||||||
private readonly idTokenClaims: IdTokenClaims,
|
private readonly idTokenClaims: IdTokenClaims,
|
||||||
) {
|
) {
|
||||||
this.oidcClientReady = this.initialiseOidcClient(authConfig, clientId, deviceId, redirectUri);
|
this.oidcClientReady = this.initialiseOidcClient(issuer, clientId, deviceId, redirectUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initialiseOidcClient(
|
private async initialiseOidcClient(
|
||||||
authConfig: IDelegatedAuthConfig,
|
issuer: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
redirectUri: string,
|
redirectUri: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const config = await discoverAndValidateAuthenticationConfig(authConfig);
|
const config = await discoverAndValidateOIDCIssuerWellKnown(issuer);
|
||||||
|
|
||||||
const scope = generateScope(deviceId);
|
const scope = generateScope(deviceId);
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
import { OidcMetadata, SigninResponse } from "oidc-client-ts";
|
import { OidcMetadata, SigninResponse } from "oidc-client-ts";
|
||||||
|
|
||||||
import { IDelegatedAuthConfig } from "../client";
|
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { OidcError } from "./error";
|
import { OidcError } from "./error";
|
||||||
|
|
||||||
@ -35,31 +34,6 @@ export type ValidatedIssuerConfig = {
|
|||||||
accountManagementActionsSupported?: string[];
|
accountManagementActionsSupported?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates MSC2965 m.authentication config
|
|
||||||
* Returns valid configuration
|
|
||||||
* @param wellKnown - client well known as returned from ./well-known/client/matrix
|
|
||||||
* @returns config - when present and valid
|
|
||||||
* @throws when config is not found or invalid
|
|
||||||
*/
|
|
||||||
export const validateWellKnownAuthentication = (authentication?: IDelegatedAuthConfig): IDelegatedAuthConfig => {
|
|
||||||
if (!authentication) {
|
|
||||||
throw new Error(OidcError.NotSupported);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof authentication.issuer === "string" &&
|
|
||||||
(!authentication.hasOwnProperty("account") || typeof authentication.account === "string")
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
issuer: authentication.issuer,
|
|
||||||
account: authentication.account,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(OidcError.Misconfigured);
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
||||||
const requiredStringProperty = (wellKnown: Record<string, unknown>, key: string): boolean => {
|
const requiredStringProperty = (wellKnown: Record<string, unknown>, key: string): boolean => {
|
||||||
@ -150,7 +124,11 @@ export type ValidatedIssuerMetadata = Partial<OidcMetadata> &
|
|||||||
| "response_types_supported"
|
| "response_types_supported"
|
||||||
| "grant_types_supported"
|
| "grant_types_supported"
|
||||||
| "code_challenge_methods_supported"
|
| "code_challenge_methods_supported"
|
||||||
>;
|
> & {
|
||||||
|
// MSC2965 extensions to the OIDC spec
|
||||||
|
account_management_uri?: string;
|
||||||
|
account_management_actions_supported?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps validateOIDCIssuerWellKnown in a type assertion
|
* Wraps validateOIDCIssuerWellKnown in a type assertion
|
||||||
|
Reference in New Issue
Block a user