UNPKG

5.01 kBPlain TextView Raw
1/*
2 * Copyright 2021 Inrupt Inc.
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal in
6 * the Software without restriction, including without limitation the rights to use,
7 * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8 * Software, and to permit persons to whom the Software is furnished to do so,
9 * subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 */
21
22/**
23 * @hidden
24 * @packageDocumentation
25 */
26
27import { inject, injectable } from "tsyringe";
28import {
29 IClientRegistrar,
30 IStorageUtility,
31 IIssuerConfigFetcher,
32} from "@inrupt/solid-client-authn-core";
33import formurlencoded from "form-urlencoded";
34import {
35 generateJwkForDpop,
36 createDpopHeader,
37 decodeJwt,
38} from "@inrupt/oidc-client-ext";
39
40/**
41 * @hidden
42 */
43export interface ITokenRequester {
44 request(localUserId: string, body: Record<string, string>): Promise<void>;
45}
46
47function btoa(str: string): string {
48 return Buffer.from(str.toString(), "binary").toString("base64");
49}
50
51// NOTE: The code from this class will soon move to oidc-client-dpop-browser
52
53/**
54 * @hidden
55 */
56@injectable()
57export 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 // Get the issuer config to find the token url
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 // Check that this issuer supports the provided request
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 // Make request
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 // eslint-disable-next-line camelcase
117 client_id: client.clientId,
118 }),
119 };
120
121 if (client.clientSecret) {
122 // TODO: Support DPoP-bound refresh tokens
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