1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | import { inject, injectable } from "tsyringe";
|
28 | import {
|
29 | IClientRegistrar,
|
30 | IStorageUtility,
|
31 | IIssuerConfigFetcher,
|
32 | } from "@inrupt/solid-client-authn-core";
|
33 | import formurlencoded from "form-urlencoded";
|
34 | import {
|
35 | generateJwkForDpop,
|
36 | createDpopHeader,
|
37 | decodeJwt,
|
38 | } from "@inrupt/oidc-client-ext";
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | export interface ITokenRequester {
|
44 | request(localUserId: string, body: Record<string, string>): Promise<void>;
|
45 | }
|
46 |
|
47 | function btoa(str: string): string {
|
48 | return Buffer.from(str.toString(), "binary").toString("base64");
|
49 | }
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | @injectable()
|
57 | export default class TokenRequester {
|
58 | constructor(
|
59 | @inject("storageUtility") private storageUtility: IStorageUtility,
|
60 | @inject("issuerConfigFetcher")
|
61 | private issuerConfigFetcher: IIssuerConfigFetcher,
|
62 | @inject("clientRegistrar") private clientRegistrar: IClientRegistrar
|
63 | ) {}
|
64 |
|
65 | async request(
|
66 | sessionId: string,
|
67 | body: Record<string, string>
|
68 | ): Promise<void> {
|
69 | const [issuer] = await Promise.all([
|
70 | this.storageUtility.getForUser(sessionId, "issuer", {
|
71 | errorIfNull: true,
|
72 | }),
|
73 | ]);
|
74 |
|
75 |
|
76 | const issuerConfig = await this.issuerConfigFetcher.fetchConfig(
|
77 | issuer as string
|
78 | );
|
79 |
|
80 | const client = await this.clientRegistrar.getClient(
|
81 | { sessionId },
|
82 | issuerConfig
|
83 | );
|
84 |
|
85 |
|
86 | if (
|
87 | body.grant_type &&
|
88 | (!issuerConfig.grantTypesSupported ||
|
89 | !issuerConfig.grantTypesSupported.includes(body.grant_type))
|
90 | ) {
|
91 | throw new Error(
|
92 | `The issuer [${issuer}] does not support the [${body.grant_type}] grant`
|
93 | );
|
94 | }
|
95 | if (!issuerConfig.tokenEndpoint) {
|
96 | throw new Error(`This issuer [${issuer}] does not have a token endpoint`);
|
97 | }
|
98 |
|
99 | const jsonWebKey = await generateJwkForDpop();
|
100 |
|
101 |
|
102 | const tokenRequestInit: RequestInit & {
|
103 | headers: Record<string, string>;
|
104 | } = {
|
105 | method: "POST",
|
106 | headers: {
|
107 | DPoP: await createDpopHeader(
|
108 | issuerConfig.tokenEndpoint,
|
109 | "POST",
|
110 | jsonWebKey
|
111 | ),
|
112 | "content-type": "application/x-www-form-urlencoded",
|
113 | },
|
114 | body: formurlencoded({
|
115 | ...body,
|
116 |
|
117 | client_id: client.clientId,
|
118 | }),
|
119 | };
|
120 |
|
121 | if (client.clientSecret) {
|
122 |
|
123 | tokenRequestInit.headers.Authorization = `Basic ${btoa(
|
124 | `${client.clientId}:${client.clientSecret}`
|
125 | )}`;
|
126 | }
|
127 |
|
128 | const tokenResponse = await (
|
129 | await window.fetch(issuerConfig.tokenEndpoint, tokenRequestInit)
|
130 | ).json();
|
131 |
|
132 | // Check the response
|
133 | if (
|
134 | !(
|
135 | tokenResponse &&
|
136 | tokenResponse.access_token &&
|
137 | tokenResponse.id_token &&
|
138 | typeof tokenResponse.access_token === "string" &&
|
139 | typeof tokenResponse.id_token === "string" &&
|
140 | (!tokenResponse.refresh_token ||
|
141 | typeof tokenResponse.refresh_token === "string")
|
142 | )
|
143 | ) {
|
144 | throw new Error("IDP token route returned an invalid response.");
|
145 | }
|
146 |
|
147 | const decoded = await decodeJwt(tokenResponse.access_token as string);
|
148 | if (!decoded || !decoded.sub) {
|
149 | throw new Error(
|
150 | "The Authorization Server returned a bad token (i.e. when decoded we did not find the required 'sub' claim)."
|
151 | );
|
152 | }
|
153 |
|
154 | await this.storageUtility.setForUser(
|
155 | sessionId,
|
156 | {
|
157 | accessToken: tokenResponse.access_token as string,
|
158 | idToken: tokenResponse.id_token as string,
|
159 | refreshToken: tokenResponse.refresh_token as string,
|
160 | webId: decoded.sub as string,
|
161 | isLoggedIn: "true",
|
162 | },
|
163 | { secure: true }
|
164 | );
|
165 | }
|
166 | }
|
167 |
|
\ | No newline at end of file |