@inrupt/solid-client-authn-browser 1.17.5 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ClientAuthentication.d.ts +1 -0
- package/dist/Session.d.ts +4 -67
- package/dist/defaultSession.d.ts +0 -28
- package/dist/dependencies.d.ts +1 -2
- package/dist/index.d.ts +2 -3
- package/dist/index.js +40 -153
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +34 -143
- package/dist/index.mjs.map +1 -1
- package/dist/login/oidc/__mocks__/IOidcOptions.d.ts +2 -0
- package/dist/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.d.ts +4 -0
- package/dist/solid-client-authn.bundle.js +19 -19
- package/dist/solid-client-authn.bundle.js.map +1 -1
- package/package.json +7 -8
- package/rollup.config.mjs +28 -14
- package/src/ClientAuthentication.spec.ts +1 -13
- package/src/Session.spec.ts +11 -178
- package/src/Session.ts +6 -98
- package/src/defaultSession.spec.ts +0 -18
- package/src/defaultSession.ts +0 -40
- package/src/dependencies.ts +1 -2
- package/src/index.spec.ts +1 -2
- package/src/index.ts +9 -3
- package/src/login/oidc/ClientRegistrar.spec.ts +9 -9
- package/src/login/oidc/ClientRegistrar.ts +15 -8
- package/src/login/oidc/IssuerConfigFetcher.spec.ts +17 -32
- package/src/login/oidc/IssuerConfigFetcher.ts +1 -5
- package/src/login/oidc/OidcLoginHandler.spec.ts +6 -3
- package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.spec.ts +28 -42
- package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts +18 -34
- package/src/login/oidc/incomingRedirectHandler/ErrorOidcHandler.spec.ts +0 -11
- package/src/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.spec.ts +1 -12
- package/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.spec.ts +1 -0
- package/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.ts +1 -2
- package/.prettierignore +0 -3
- package/sampleFlows/SolidAuthClientArchitecture.png +0 -0
- package/sampleFlows/authorization-code-with-pkce.png +0 -0
- package/sampleFlows/authorization-code-with-pkce.seqdiag +0 -46
- package/sampleFlows/authorization-code.png +0 -0
- package/sampleFlows/authorization-code.seqdiag +0 -50
- package/sampleFlows/client-credentials.png +0 -0
- package/sampleFlows/client-credentials.seqdiag +0 -38
- package/sampleFlows/legacy-implicit-flow.png +0 -0
- package/sampleFlows/legacy-implicit-flow.seqdiag +0 -43
- package/sampleFlows/self-signed.png +0 -0
- package/sampleFlows/self-signed.seqdiag +0 -49
- package/sampleFlows/signatory-access-control.seqdiag +0 -36
- package/sampleFlows/signatory-access-control.svg +0 -1
- package/sonar-project.properties +0 -15
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
mockStorageUtility,
|
|
25
25
|
} from "@inrupt/solid-client-authn-core/mocks";
|
|
26
26
|
import { jest, it, describe, expect } from "@jest/globals";
|
|
27
|
-
import { Response as NodeResponse } from "@inrupt/universal-fetch";
|
|
28
27
|
import ClientRegistrar from "./ClientRegistrar";
|
|
29
28
|
import { IssuerConfigFetcherFetchConfigResponse } from "./__mocks__/IssuerConfigFetcher";
|
|
30
29
|
|
|
@@ -47,14 +46,14 @@ describe("ClientRegistrar", () => {
|
|
|
47
46
|
// actual OIDC provider response.
|
|
48
47
|
const mockFetch = (jest.fn() as any).mockResolvedValueOnce(
|
|
49
48
|
/* eslint-disable camelcase */
|
|
50
|
-
new
|
|
49
|
+
new Response(
|
|
51
50
|
JSON.stringify({
|
|
52
51
|
client_id: "abcd",
|
|
53
52
|
client_secret: "1234",
|
|
54
53
|
redirect_uris: ["https://example.com"],
|
|
55
54
|
id_token_signed_response_alg: "RS256",
|
|
56
55
|
}),
|
|
57
|
-
)
|
|
56
|
+
),
|
|
58
57
|
/* eslint-enable camelcase */
|
|
59
58
|
);
|
|
60
59
|
global.fetch = mockFetch;
|
|
@@ -98,12 +97,12 @@ describe("ClientRegistrar", () => {
|
|
|
98
97
|
it("can register a public client without secret", async () => {
|
|
99
98
|
const mockFetch = (jest.fn() as any).mockResolvedValueOnce(
|
|
100
99
|
/* eslint-disable camelcase */
|
|
101
|
-
new
|
|
100
|
+
new Response(
|
|
102
101
|
JSON.stringify({
|
|
103
102
|
client_id: "abcd",
|
|
104
103
|
redirect_uris: ["https://example.com"],
|
|
105
104
|
}),
|
|
106
|
-
)
|
|
105
|
+
),
|
|
107
106
|
/* eslint-enable camelcase */
|
|
108
107
|
);
|
|
109
108
|
global.fetch = mockFetch;
|
|
@@ -144,9 +143,9 @@ describe("ClientRegistrar", () => {
|
|
|
144
143
|
it("handles a failure to dynamically register elegantly", async () => {
|
|
145
144
|
const mockFetch = (jest.fn() as any).mockResolvedValueOnce(
|
|
146
145
|
/* eslint-disable camelcase */
|
|
147
|
-
new
|
|
146
|
+
new Response('{"error":"bad stuff that\'s an error"}', {
|
|
148
147
|
status: 400,
|
|
149
|
-
})
|
|
148
|
+
}),
|
|
150
149
|
/* eslint-enable camelcase */
|
|
151
150
|
);
|
|
152
151
|
global.fetch = mockFetch;
|
|
@@ -177,6 +176,7 @@ describe("ClientRegistrar", () => {
|
|
|
177
176
|
"solidClientAuthenticationUser:mySession": {
|
|
178
177
|
clientId: "an id",
|
|
179
178
|
clientSecret: "a secret",
|
|
179
|
+
clientType: "dynamic",
|
|
180
180
|
},
|
|
181
181
|
},
|
|
182
182
|
false,
|
|
@@ -198,13 +198,13 @@ describe("ClientRegistrar", () => {
|
|
|
198
198
|
it("saves dynamic registration information", async () => {
|
|
199
199
|
const mockFetch = (jest.fn() as any).mockResolvedValueOnce(
|
|
200
200
|
/* eslint-disable camelcase */
|
|
201
|
-
new
|
|
201
|
+
new Response(
|
|
202
202
|
JSON.stringify({
|
|
203
203
|
client_id: "some id",
|
|
204
204
|
client_secret: "some secret",
|
|
205
205
|
redirect_uris: ["https://example.com"],
|
|
206
206
|
}),
|
|
207
|
-
)
|
|
207
|
+
),
|
|
208
208
|
/* eslint-enable camelcase */
|
|
209
209
|
);
|
|
210
210
|
global.fetch = mockFetch;
|
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
IClient,
|
|
32
32
|
IClientRegistrarOptions,
|
|
33
33
|
} from "@inrupt/solid-client-authn-core";
|
|
34
|
+
import { isKnownClientType } from "@inrupt/solid-client-authn-core";
|
|
34
35
|
import { registerClient } from "@inrupt/oidc-client-ext";
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -49,7 +50,8 @@ export default class ClientRegistrar implements IClientRegistrar {
|
|
|
49
50
|
const [
|
|
50
51
|
storedClientId,
|
|
51
52
|
storedClientSecret,
|
|
52
|
-
|
|
53
|
+
storedClientName,
|
|
54
|
+
storedClientType,
|
|
53
55
|
] = await Promise.all([
|
|
54
56
|
this.storageUtility.getForUser(options.sessionId, "clientId", {
|
|
55
57
|
secure: false,
|
|
@@ -57,17 +59,21 @@ export default class ClientRegistrar implements IClientRegistrar {
|
|
|
57
59
|
this.storageUtility.getForUser(options.sessionId, "clientSecret", {
|
|
58
60
|
secure: false,
|
|
59
61
|
}),
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
this.storageUtility.getForUser(options.sessionId, "clientName", {
|
|
63
|
+
secure: false,
|
|
64
|
+
}),
|
|
65
|
+
this.storageUtility.getForUser(options.sessionId, "clientType", {
|
|
66
|
+
secure: false,
|
|
67
|
+
}),
|
|
64
68
|
]);
|
|
65
|
-
if (storedClientId) {
|
|
69
|
+
if (storedClientId && isKnownClientType(storedClientType)) {
|
|
66
70
|
return {
|
|
67
71
|
clientId: storedClientId,
|
|
68
72
|
clientSecret: storedClientSecret,
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
clientName: storedClientName,
|
|
74
|
+
// Note: static clients are not applicable in a browser context.
|
|
75
|
+
clientType: storedClientType,
|
|
76
|
+
} as IClient;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
try {
|
|
@@ -75,6 +81,7 @@ export default class ClientRegistrar implements IClientRegistrar {
|
|
|
75
81
|
// Save info
|
|
76
82
|
const infoToSave: Record<string, string> = {
|
|
77
83
|
clientId: registeredClient.clientId,
|
|
84
|
+
clientType: "dynamic",
|
|
78
85
|
};
|
|
79
86
|
if (registeredClient.clientSecret) {
|
|
80
87
|
infoToSave.clientSecret = registeredClient.clientSecret;
|
|
@@ -21,24 +21,22 @@
|
|
|
21
21
|
|
|
22
22
|
import { jest, it, describe, expect } from "@jest/globals";
|
|
23
23
|
import { mockStorageUtility } from "@inrupt/solid-client-authn-core";
|
|
24
|
-
import { Response as NodeResponse } from "@inrupt/universal-fetch";
|
|
25
|
-
import type * as UniversalFetch from "@inrupt/universal-fetch";
|
|
26
24
|
import IssuerConfigFetcher from "./IssuerConfigFetcher";
|
|
27
25
|
|
|
28
|
-
jest.mock("@inrupt/universal-fetch", () => {
|
|
29
|
-
const fetchModule = jest.requireActual(
|
|
30
|
-
"@inrupt/universal-fetch",
|
|
31
|
-
) as typeof UniversalFetch;
|
|
32
|
-
return {
|
|
33
|
-
...fetchModule,
|
|
34
|
-
fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(),
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
|
|
38
26
|
/**
|
|
39
27
|
* Test for IssuerConfigFetcher
|
|
40
28
|
*/
|
|
41
29
|
describe("IssuerConfigFetcher", () => {
|
|
30
|
+
let mockFetch: jest.SpiedFunction<typeof fetch>;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
mockFetch = jest.spyOn(globalThis, "fetch");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
jest.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
42
40
|
const defaultMocks = {
|
|
43
41
|
storageUtility: mockStorageUtility({}),
|
|
44
42
|
};
|
|
@@ -55,11 +53,9 @@ describe("IssuerConfigFetcher", () => {
|
|
|
55
53
|
const configFetcher = getIssuerConfigFetcher({
|
|
56
54
|
storageUtility: mockStorageUtility({}),
|
|
57
55
|
});
|
|
58
|
-
|
|
59
|
-
"@inrupt/universal-fetch",
|
|
60
|
-
) as jest.Mocked<typeof UniversalFetch>;
|
|
56
|
+
|
|
61
57
|
mockFetch.mockResolvedValueOnce(
|
|
62
|
-
new
|
|
58
|
+
new Response(
|
|
63
59
|
JSON.stringify({
|
|
64
60
|
issuer: "https://example.com",
|
|
65
61
|
// eslint-disable-next-line camelcase
|
|
@@ -83,10 +79,7 @@ describe("IssuerConfigFetcher", () => {
|
|
|
83
79
|
});
|
|
84
80
|
|
|
85
81
|
it("should throw an error if the fetched config could not be converted to JSON", async () => {
|
|
86
|
-
|
|
87
|
-
"@inrupt/universal-fetch",
|
|
88
|
-
) as jest.Mocked<typeof UniversalFetch>;
|
|
89
|
-
mockFetch.mockResolvedValueOnce(new NodeResponse("Not JSON."));
|
|
82
|
+
mockFetch.mockResolvedValueOnce(new Response("Not JSON."));
|
|
90
83
|
const configFetcher = new IssuerConfigFetcher(mockStorageUtility({}));
|
|
91
84
|
|
|
92
85
|
await expect(configFetcher.fetchConfig("https://some.url")).rejects.toThrow(
|
|
@@ -95,11 +88,8 @@ describe("IssuerConfigFetcher", () => {
|
|
|
95
88
|
});
|
|
96
89
|
|
|
97
90
|
it("should return a config including the support for solid-oidc if present in the discovery profile", async () => {
|
|
98
|
-
const { fetch: mockFetch } = jest.requireMock(
|
|
99
|
-
"@inrupt/universal-fetch",
|
|
100
|
-
) as jest.Mocked<typeof UniversalFetch>;
|
|
101
91
|
mockFetch.mockResolvedValueOnce(
|
|
102
|
-
new
|
|
92
|
+
new Response(
|
|
103
93
|
JSON.stringify({
|
|
104
94
|
issuer: "https://example.com",
|
|
105
95
|
// eslint-disable-next-line camelcase
|
|
@@ -120,11 +110,8 @@ describe("IssuerConfigFetcher", () => {
|
|
|
120
110
|
});
|
|
121
111
|
|
|
122
112
|
it("should return a default value for the supported scopes if not advertized by the OpenID provider", async () => {
|
|
123
|
-
const { fetch: mockFetch } = jest.requireMock(
|
|
124
|
-
"@inrupt/universal-fetch",
|
|
125
|
-
) as jest.Mocked<typeof UniversalFetch>;
|
|
126
113
|
mockFetch.mockResolvedValueOnce(
|
|
127
|
-
new
|
|
114
|
+
new Response(
|
|
128
115
|
JSON.stringify({
|
|
129
116
|
issuer: "https://example.com",
|
|
130
117
|
// eslint-disable-next-line camelcase
|
|
@@ -143,12 +130,10 @@ describe("IssuerConfigFetcher", () => {
|
|
|
143
130
|
|
|
144
131
|
it("should append the .well-known/openid-configuration path at the end of the issuer URL", async () => {
|
|
145
132
|
// The response value is irrelevant to this test.
|
|
146
|
-
|
|
147
|
-
"@inrupt/universal-fetch",
|
|
148
|
-
) as jest.Mocked<typeof UniversalFetch>;
|
|
133
|
+
|
|
149
134
|
mockFetch.mockImplementation(
|
|
150
135
|
async () =>
|
|
151
|
-
new
|
|
136
|
+
new Response(
|
|
152
137
|
JSON.stringify({
|
|
153
138
|
issuer: "https://example.com",
|
|
154
139
|
// eslint-disable-next-line camelcase
|
|
@@ -33,7 +33,6 @@ import type {
|
|
|
33
33
|
IStorageUtility,
|
|
34
34
|
} from "@inrupt/solid-client-authn-core";
|
|
35
35
|
import { ConfigurationError } from "@inrupt/solid-client-authn-core";
|
|
36
|
-
import { fetch as uniFetch } from "@inrupt/universal-fetch";
|
|
37
36
|
|
|
38
37
|
export const WELL_KNOWN_OPENID_CONFIG = ".well-known/openid-configuration";
|
|
39
38
|
|
|
@@ -172,10 +171,7 @@ export default class IssuerConfigFetcher implements IIssuerConfigFetcher {
|
|
|
172
171
|
// includes the full issuer path. See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig.
|
|
173
172
|
issuer.endsWith("/") ? issuer : `${issuer}/`,
|
|
174
173
|
).href;
|
|
175
|
-
const issuerConfigRequestBody = await
|
|
176
|
-
globalThis,
|
|
177
|
-
openIdConfigUrl,
|
|
178
|
-
);
|
|
174
|
+
const issuerConfigRequestBody = await fetch(openIdConfigUrl);
|
|
179
175
|
// Check the validity of the fetched config
|
|
180
176
|
try {
|
|
181
177
|
issuerConfig = processConfig(await issuerConfigRequestBody.json());
|
|
@@ -77,9 +77,10 @@ describe("OidcLoginHandler", () => {
|
|
|
77
77
|
mockStorage({}),
|
|
78
78
|
mockStorage({
|
|
79
79
|
"solidClientAuthenticationUser:mySession": {
|
|
80
|
-
// The value of the client ID doesn't matter
|
|
80
|
+
// The value of the client ID doesn't matter.
|
|
81
81
|
// This checks it gets passed through from storage to the handler.
|
|
82
|
-
clientId: "some
|
|
82
|
+
clientId: "https://example.org/some-client-id",
|
|
83
|
+
clientType: "solid-oidc",
|
|
83
84
|
},
|
|
84
85
|
}),
|
|
85
86
|
);
|
|
@@ -99,7 +100,9 @@ describe("OidcLoginHandler", () => {
|
|
|
99
100
|
expect(actualHandler.handle.mock.calls).toHaveLength(1);
|
|
100
101
|
|
|
101
102
|
const calledWith = actualHandler.handle.mock.calls[0][0];
|
|
102
|
-
expect(calledWith.client.clientId).toBe(
|
|
103
|
+
expect(calledWith.client.clientId).toBe(
|
|
104
|
+
"https://example.org/some-client-id",
|
|
105
|
+
);
|
|
103
106
|
});
|
|
104
107
|
|
|
105
108
|
it("should lookup client ID if not provided, if not found do DCR", async () => {
|
|
@@ -33,10 +33,7 @@ import {
|
|
|
33
33
|
mockStorageUtility,
|
|
34
34
|
} from "@inrupt/solid-client-authn-core/mocks";
|
|
35
35
|
import { jest, it, describe, expect } from "@jest/globals";
|
|
36
|
-
import { getBearerToken } from "@inrupt/oidc-client-ext";
|
|
37
36
|
import type * as OidcClientExt from "@inrupt/oidc-client-ext";
|
|
38
|
-
import { fetch, Response, Headers } from "@inrupt/universal-fetch";
|
|
39
|
-
import type * as UniversalFetch from "@inrupt/universal-fetch";
|
|
40
37
|
import type { JWK } from "jose";
|
|
41
38
|
import { importJWK } from "jose";
|
|
42
39
|
import type { KeyObject } from "crypto";
|
|
@@ -48,15 +45,7 @@ import {
|
|
|
48
45
|
mockTokenRefresher,
|
|
49
46
|
} from "../refresh/__mocks__/TokenRefresher";
|
|
50
47
|
|
|
51
|
-
jest.
|
|
52
|
-
const fetchModule = jest.requireActual(
|
|
53
|
-
"@inrupt/universal-fetch",
|
|
54
|
-
) as typeof UniversalFetch;
|
|
55
|
-
return {
|
|
56
|
-
...fetchModule,
|
|
57
|
-
fetch: jest.fn<typeof fetch>(),
|
|
58
|
-
};
|
|
59
|
-
});
|
|
48
|
+
const mockedFetch = jest.spyOn(globalThis, "fetch");
|
|
60
49
|
|
|
61
50
|
const mockJwk = (): JWK => {
|
|
62
51
|
return {
|
|
@@ -160,16 +149,22 @@ jest.mock("@inrupt/oidc-client-ext");
|
|
|
160
149
|
|
|
161
150
|
jest.useFakeTimers();
|
|
162
151
|
|
|
163
|
-
function mockOidcClient(
|
|
152
|
+
function mockOidcClient(
|
|
153
|
+
tokenType: "dpop" | "bearer" = "dpop",
|
|
154
|
+
): typeof OidcClientExt {
|
|
164
155
|
const mockedOidcClient = jest.requireMock<typeof OidcClientExt>(
|
|
165
156
|
"@inrupt/oidc-client-ext",
|
|
166
157
|
);
|
|
167
|
-
mockedOidcClient.
|
|
168
|
-
.fn<typeof mockedOidcClient.
|
|
169
|
-
.mockImplementationOnce(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
158
|
+
mockedOidcClient.getTokens = jest
|
|
159
|
+
.fn<typeof mockedOidcClient.getTokens>()
|
|
160
|
+
.mockImplementationOnce(
|
|
161
|
+
tokenType === "dpop"
|
|
162
|
+
? mockTokenEndpointDpopResponse
|
|
163
|
+
: () =>
|
|
164
|
+
new Promise<OidcClientExt.CodeExchangeResult>((resolve) => {
|
|
165
|
+
resolve(mockTokenEndpointBearerResponse());
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
173
168
|
mockedOidcClient.refresh = jest
|
|
174
169
|
.fn<typeof mockedOidcClient.refresh>()
|
|
175
170
|
.mockResolvedValueOnce({
|
|
@@ -202,9 +197,6 @@ function mockClientRegistrar(client: IClient): IClientRegistrar {
|
|
|
202
197
|
}
|
|
203
198
|
|
|
204
199
|
const mockFetch = (response: Response) => {
|
|
205
|
-
const { fetch: mockedFetch } = jest.requireMock(
|
|
206
|
-
"@inrupt/universal-fetch",
|
|
207
|
-
) as jest.Mocked<typeof UniversalFetch>;
|
|
208
200
|
mockedFetch.mockResolvedValueOnce(response);
|
|
209
201
|
return mockedFetch;
|
|
210
202
|
};
|
|
@@ -331,13 +323,14 @@ describe("AuthCodeRedirectHandler", () => {
|
|
|
331
323
|
|
|
332
324
|
await authCodeRedirectHandler.handle(mockRedirectUrl());
|
|
333
325
|
|
|
334
|
-
expect(mockedOidcClient.
|
|
326
|
+
expect(mockedOidcClient.getTokens).toHaveBeenCalledWith(
|
|
335
327
|
expect.anything(),
|
|
336
328
|
expect.anything(),
|
|
337
329
|
expect.objectContaining({
|
|
338
330
|
codeVerifier: DEFAULT_CODE_VERIFIER,
|
|
339
331
|
redirectUrl: DEFAULT_REDIRECT_URL,
|
|
340
332
|
}),
|
|
333
|
+
expect.anything(),
|
|
341
334
|
);
|
|
342
335
|
});
|
|
343
336
|
|
|
@@ -415,8 +408,7 @@ describe("AuthCodeRedirectHandler", () => {
|
|
|
415
408
|
});
|
|
416
409
|
|
|
417
410
|
it("returns an authenticated bearer fetch if requested", async () => {
|
|
418
|
-
mockOidcClient();
|
|
419
|
-
const mockedFetch = jest.mocked<typeof fetch>(fetch);
|
|
411
|
+
mockOidcClient("bearer");
|
|
420
412
|
mockedFetch.mockResolvedValueOnce(
|
|
421
413
|
new Response("", {
|
|
422
414
|
status: 200,
|
|
@@ -427,9 +419,8 @@ describe("AuthCodeRedirectHandler", () => {
|
|
|
427
419
|
storageUtility: mockDefaultStorageUtility({ dpop: false }),
|
|
428
420
|
});
|
|
429
421
|
|
|
430
|
-
const redirectInfo =
|
|
431
|
-
mockRedirectUrl()
|
|
432
|
-
);
|
|
422
|
+
const redirectInfo =
|
|
423
|
+
await authCodeRedirectHandler.handle(mockRedirectUrl());
|
|
433
424
|
|
|
434
425
|
// This will call the oidc-client-ext module, which is mocked at
|
|
435
426
|
// the top of this file. The purpose of this test is to check that, if
|
|
@@ -444,7 +435,6 @@ describe("AuthCodeRedirectHandler", () => {
|
|
|
444
435
|
|
|
445
436
|
it("returns an authenticated DPoP fetch if requested", async () => {
|
|
446
437
|
mockOidcClient();
|
|
447
|
-
const mockedFetch = jest.mocked<typeof fetch>(fetch);
|
|
448
438
|
mockedFetch.mockResolvedValueOnce(
|
|
449
439
|
new Response("", {
|
|
450
440
|
status: 200,
|
|
@@ -454,9 +444,8 @@ describe("AuthCodeRedirectHandler", () => {
|
|
|
454
444
|
const authCodeRedirectHandler = getAuthCodeRedirectHandler({
|
|
455
445
|
storageUtility: mockDefaultStorageUtility({ dpop: true }),
|
|
456
446
|
});
|
|
457
|
-
const redirectInfo =
|
|
458
|
-
mockRedirectUrl()
|
|
459
|
-
);
|
|
447
|
+
const redirectInfo =
|
|
448
|
+
await authCodeRedirectHandler.handle(mockRedirectUrl());
|
|
460
449
|
await redirectInfo.fetch("https://some.other.url");
|
|
461
450
|
|
|
462
451
|
const header = new Headers(mockedFetch.mock.calls[0][1]?.headers);
|
|
@@ -525,9 +514,8 @@ describe("AuthCodeRedirectHandler", () => {
|
|
|
525
514
|
storageUtility: mockDefaultStorageUtility(),
|
|
526
515
|
});
|
|
527
516
|
|
|
528
|
-
const sessionInfo =
|
|
529
|
-
mockRedirectUrl()
|
|
530
|
-
);
|
|
517
|
+
const sessionInfo =
|
|
518
|
+
await authCodeRedirectHandler.handle(mockRedirectUrl());
|
|
531
519
|
|
|
532
520
|
expect(sessionInfo.expirationDate).toBe(
|
|
533
521
|
MOCK_TIMESTAMP + MOCK_EXPIRE_TIME * 1000,
|
|
@@ -535,10 +523,10 @@ describe("AuthCodeRedirectHandler", () => {
|
|
|
535
523
|
});
|
|
536
524
|
|
|
537
525
|
it("returns null for the expiration time if none was provided", async () => {
|
|
538
|
-
const mockedOidcClient = mockOidcClient();
|
|
526
|
+
const mockedOidcClient = mockOidcClient("bearer");
|
|
539
527
|
mockFetch(new Response());
|
|
540
|
-
mockedOidcClient.
|
|
541
|
-
.fn(
|
|
528
|
+
mockedOidcClient.getTokens = jest
|
|
529
|
+
.fn<(typeof OidcClientExt)["getTokens"]>()
|
|
542
530
|
.mockResolvedValueOnce({
|
|
543
531
|
accessToken: mockAccessTokenBearer(),
|
|
544
532
|
idToken: mockIdToken(),
|
|
@@ -552,9 +540,8 @@ describe("AuthCodeRedirectHandler", () => {
|
|
|
552
540
|
storageUtility: mockedStorage,
|
|
553
541
|
});
|
|
554
542
|
|
|
555
|
-
const sessionInfo =
|
|
556
|
-
mockRedirectUrl()
|
|
557
|
-
);
|
|
543
|
+
const sessionInfo =
|
|
544
|
+
await authCodeRedirectHandler.handle(mockRedirectUrl());
|
|
558
545
|
|
|
559
546
|
expect(sessionInfo.expirationDate).toBeUndefined();
|
|
560
547
|
});
|
|
@@ -598,7 +585,6 @@ describe("AuthCodeRedirectHandler", () => {
|
|
|
598
585
|
await authCodeRedirectHandler.handle(mockRedirectUrl());
|
|
599
586
|
|
|
600
587
|
expect(mockAuthenticatedFetchBuild).toHaveBeenCalledWith(
|
|
601
|
-
expect.anything(),
|
|
602
588
|
expect.anything(),
|
|
603
589
|
expect.objectContaining({
|
|
604
590
|
refreshOptions: {
|
|
@@ -23,12 +23,6 @@
|
|
|
23
23
|
* @hidden
|
|
24
24
|
* @packageDocumentation
|
|
25
25
|
*/
|
|
26
|
-
// FIXME: The following doesn't work in the browser, it results in all the fetches
|
|
27
|
-
// being unauthenticated. This should be looked into when migrating to universal-fetch.
|
|
28
|
-
// import { fetch } from "cross-fetch";
|
|
29
|
-
|
|
30
|
-
import { fetch as uniFetch } from "@inrupt/universal-fetch";
|
|
31
|
-
|
|
32
26
|
import type {
|
|
33
27
|
IClient,
|
|
34
28
|
IClientRegistrar,
|
|
@@ -45,13 +39,9 @@ import {
|
|
|
45
39
|
loadOidcContextFromStorage,
|
|
46
40
|
maybeBuildRpInitiatedLogout,
|
|
47
41
|
} from "@inrupt/solid-client-authn-core";
|
|
48
|
-
import
|
|
49
|
-
import { getDpopToken, getBearerToken } from "@inrupt/oidc-client-ext";
|
|
42
|
+
import { getTokens } from "@inrupt/oidc-client-ext";
|
|
50
43
|
import type { EventEmitter } from "events";
|
|
51
44
|
|
|
52
|
-
const globalFetch: typeof uniFetch = (...args) =>
|
|
53
|
-
uniFetch.call(globalThis, ...args);
|
|
54
|
-
|
|
55
45
|
/**
|
|
56
46
|
* @hidden
|
|
57
47
|
*/
|
|
@@ -141,26 +131,24 @@ export class AuthCodeRedirectHandler implements IIncomingRedirectHandler {
|
|
|
141
131
|
issuerConfig,
|
|
142
132
|
);
|
|
143
133
|
|
|
144
|
-
let tokens: CodeExchangeResult;
|
|
145
134
|
const tokenCreatedAt = Date.now();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
135
|
+
const tokens = await getTokens(
|
|
136
|
+
issuerConfig,
|
|
137
|
+
client,
|
|
138
|
+
{
|
|
149
139
|
grantType: "authorization_code",
|
|
150
140
|
// We rely on our 'canHandle' function checking that the OAuth 'code'
|
|
151
141
|
// parameter is present in our query string.
|
|
152
142
|
code: url.searchParams.get("code") as string,
|
|
153
143
|
codeVerifier,
|
|
154
144
|
redirectUrl: storedRedirectIri,
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
tokens = await getBearerToken(url.toString());
|
|
163
|
-
}
|
|
145
|
+
},
|
|
146
|
+
isDpop,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Delete oidc-client-specific session information from storage. oidc-client
|
|
150
|
+
// is no longer used for the token exchange, so it doesn't perform this automatically.
|
|
151
|
+
window.localStorage.removeItem(`oidc.${oauthState}`);
|
|
164
152
|
|
|
165
153
|
let refreshOptions: RefreshOptions | undefined;
|
|
166
154
|
if (tokens.refreshToken !== undefined) {
|
|
@@ -171,16 +159,12 @@ export class AuthCodeRedirectHandler implements IIncomingRedirectHandler {
|
|
|
171
159
|
};
|
|
172
160
|
}
|
|
173
161
|
|
|
174
|
-
const authFetch = await buildAuthenticatedFetch(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
eventEmitter,
|
|
181
|
-
expiresIn: tokens.expiresIn,
|
|
182
|
-
},
|
|
183
|
-
);
|
|
162
|
+
const authFetch = await buildAuthenticatedFetch(tokens.accessToken, {
|
|
163
|
+
dpopKey: tokens.dpopKey,
|
|
164
|
+
refreshOptions,
|
|
165
|
+
eventEmitter,
|
|
166
|
+
expiresIn: tokens.expiresIn,
|
|
167
|
+
});
|
|
184
168
|
|
|
185
169
|
await this.storageUtility.setForUser(
|
|
186
170
|
storedSessionId,
|
|
@@ -22,19 +22,8 @@
|
|
|
22
22
|
import { jest, it, describe, expect } from "@jest/globals";
|
|
23
23
|
import { EventEmitter } from "events";
|
|
24
24
|
import { EVENTS } from "@inrupt/solid-client-authn-core";
|
|
25
|
-
import type * as UniversalFetch from "@inrupt/universal-fetch";
|
|
26
25
|
import { ErrorOidcHandler } from "./ErrorOidcHandler";
|
|
27
26
|
|
|
28
|
-
jest.mock("@inrupt/universal-fetch", () => {
|
|
29
|
-
const fetchModule = jest.requireActual(
|
|
30
|
-
"@inrupt/universal-fetch",
|
|
31
|
-
) as typeof UniversalFetch;
|
|
32
|
-
return {
|
|
33
|
-
...fetchModule,
|
|
34
|
-
fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(),
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
|
|
38
27
|
describe("ErrorOidcHandler", () => {
|
|
39
28
|
describe("canHandle", () => {
|
|
40
29
|
it("Does not handle IRIs without an error parameter", async () => {
|
|
@@ -19,20 +19,9 @@
|
|
|
19
19
|
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
20
|
//
|
|
21
21
|
|
|
22
|
-
import {
|
|
23
|
-
import type * as UniversalFetch from "@inrupt/universal-fetch";
|
|
22
|
+
import { it, describe, expect } from "@jest/globals";
|
|
24
23
|
import { FallbackRedirectHandler } from "./FallbackRedirectHandler";
|
|
25
24
|
|
|
26
|
-
jest.mock("@inrupt/universal-fetch", () => {
|
|
27
|
-
const fetchModule = jest.requireActual(
|
|
28
|
-
"@inrupt/universal-fetch",
|
|
29
|
-
) as typeof UniversalFetch;
|
|
30
|
-
return {
|
|
31
|
-
...fetchModule,
|
|
32
|
-
fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(),
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
|
|
36
25
|
describe("FallbackRedirectHandler", () => {
|
|
37
26
|
describe("canHandle", () => {
|
|
38
27
|
it("always accept the given IRI", async () => {
|
|
@@ -262,6 +262,7 @@ describe("AuthorizationCodeWithPkceOidcHandler", () => {
|
|
|
262
262
|
...standardOidcOptions,
|
|
263
263
|
client: {
|
|
264
264
|
...standardOidcOptions.client,
|
|
265
|
+
clientType: "dynamic",
|
|
265
266
|
clientSecret: "I can't cook because I only drink Soylent",
|
|
266
267
|
},
|
|
267
268
|
issuerConfiguration: {
|
|
@@ -53,8 +53,7 @@ export default class AuthorizationCodeWithPkceOidcHandler
|
|
|
53
53
|
authority: oidcLoginOptions.issuer.toString(),
|
|
54
54
|
client_id: oidcLoginOptions.client.clientId,
|
|
55
55
|
client_secret: oidcLoginOptions.client.clientSecret,
|
|
56
|
-
redirect_uri: oidcLoginOptions.redirectUrl
|
|
57
|
-
post_logout_redirect_uri: oidcLoginOptions.redirectUrl.toString(),
|
|
56
|
+
redirect_uri: oidcLoginOptions.redirectUrl,
|
|
58
57
|
response_type: "code",
|
|
59
58
|
scope: DEFAULT_SCOPES,
|
|
60
59
|
filterProtocolClaims: true,
|
package/.prettierignore
DELETED
|
Binary file
|
|
Binary file
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
title: Authorization Code Flow with PKCE
|
|
2
|
-
|
|
3
|
-
note left of RP: AUTHORIZATION
|
|
4
|
-
note over RP: 1.Alice uses the decentphotos app
|
|
5
|
-
note over RP: 2. Alice selets her OP or WebID
|
|
6
|
-
RP->Alice's Pod (RS): 2.1 Retrieve Profile
|
|
7
|
-
Alice's Pod (RS)->RP:
|
|
8
|
-
RP->Alice's OP: 3. Requests OP configuration
|
|
9
|
-
Alice's OP->RP: 4. Returns OP configuration
|
|
10
|
-
RP->Alice's OP: 5. Requests OP JWKS
|
|
11
|
-
Alice's OP->RP: 6. Returns OP JWKS
|
|
12
|
-
note over RP: 7. Generates Private/Public key pair
|
|
13
|
-
note over RP: 8. Generates PKCE nonce
|
|
14
|
-
note over RP: 9. Saves both to local storage
|
|
15
|
-
RP->Alice's OP: 10. Authorization Request
|
|
16
|
-
Alice's OP->Decentphotos WebID: 11. Retrieves WebID
|
|
17
|
-
Decentphotos WebID->Alice's OP:
|
|
18
|
-
note over Alice's OP: 11. Validates redirect protocol with WebID
|
|
19
|
-
note over Alice's OP: 12. Gets Alice's consent
|
|
20
|
-
note over Alice's OP: 13. Generates an id_vc
|
|
21
|
-
Alice's OP->RP: 12. Sends code to redirect protocol
|
|
22
|
-
RP->Alice's OP: 13. Token requst with code and nonce proof
|
|
23
|
-
note over Alice's OP: 14. Validates nonce proof
|
|
24
|
-
Alice's OP->RP: 15. Sends id_vc inside id_token
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
note left of RP: SENDING REQUEST
|
|
28
|
-
note over RP: 1. Creates a pop_token
|
|
29
|
-
RP->Bob's Pod (RS): 2. Request sent
|
|
30
|
-
note over Bob's Pod (RS): 3. Checks pop_token Audience
|
|
31
|
-
note over Bob's Pod (RS): 4. Checks client signature
|
|
32
|
-
Bob's Pod (RS)->Alice's Pod (RS): 5. Retrieves Profile
|
|
33
|
-
Alice's Pod (RS)->Bob's Pod (RS):
|
|
34
|
-
note over Bob's Pod (RS): 6. Checks Issuer
|
|
35
|
-
Bob's Pod (RS)->Alice's OP: 7. Retrieves OP configuration
|
|
36
|
-
Alice's OP->Bob's Pod (RS):
|
|
37
|
-
Bob's Pod (RS)->Alice's OP: 8. Requests JWKS
|
|
38
|
-
Alice's OP->Bob's Pod (RS):
|
|
39
|
-
note over Bob's Pod (RS): 9. Performs Authentication
|
|
40
|
-
note over Bob's Pod (RS): 10. Performs Authorization
|
|
41
|
-
Bob's Pod (RS)->RP: 11. Returns Result
|
|
42
|
-
|
|
43
|
-
note left of RP: REFRESH TOKEN
|
|
44
|
-
Bob's Pod (RS)->RP: 1. 401: token expired
|
|
45
|
-
RP->Alice's OP: 2. Refresh token auth request
|
|
46
|
-
Alice's OP->RP: 3. Return updated token
|
|
Binary file
|