1import invariant from 'invariant';
2import { Platform } from 'react-native';
3import * as Base64 from './Base64';
4import { TokenError } from './Errors';
5import { requestAsync } from './Fetch';
6import { GrantType, } from './TokenRequest.types';
7/**
8 * Returns the current time in seconds.
9 */
10export function getCurrentTimeInSeconds() {
11    return Math.floor(Date.now() / 1000);
12}
13/**
14 * Token Response.
15 *
16 * [Section 5.1](https://tools.ietf.org/html/rfc6749#section-5.1)
17 */
18export class TokenResponse {
19    /**
20     * Determines whether a token refresh request must be made to refresh the tokens
21     *
22     * @param token
23     * @param secondsMargin
24     */
25    static isTokenFresh(token,
26    /**
27     * -10 minutes in seconds
28     */
29    secondsMargin = 60 * 10 * -1) {
30        if (!token) {
31            return false;
32        }
33        if (token.expiresIn) {
34            const now = getCurrentTimeInSeconds();
35            return now < token.issuedAt + token.expiresIn + secondsMargin;
36        }
37        // if there is no expiration time but we have an access token, it is assumed to never expire
38        return true;
39    }
40    /**
41     * Creates a `TokenResponse` from query parameters returned from an `AuthRequest`.
42     *
43     * @param params
44     */
45    static fromQueryParams(params) {
46        return new TokenResponse({
47            accessToken: params.access_token,
48            refreshToken: params.refresh_token,
49            scope: params.scope,
50            state: params.state,
51            idToken: params.id_token,
52            tokenType: params.token_type,
53            expiresIn: params.expires_in,
54            issuedAt: params.issued_at,
55        });
56    }
57    accessToken;
58    tokenType;
59    expiresIn;
60    refreshToken;
61    scope;
62    state;
63    idToken;
64    issuedAt;
65    constructor(response) {
66        this.accessToken = response.accessToken;
67        this.tokenType = response.tokenType ?? 'bearer';
68        this.expiresIn = response.expiresIn;
69        this.refreshToken = response.refreshToken;
70        this.scope = response.scope;
71        this.state = response.state;
72        this.idToken = response.idToken;
73        this.issuedAt = response.issuedAt ?? getCurrentTimeInSeconds();
74    }
75    applyResponseConfig(response) {
76        this.accessToken = response.accessToken ?? this.accessToken;
77        this.tokenType = response.tokenType ?? this.tokenType ?? 'bearer';
78        this.expiresIn = response.expiresIn ?? this.expiresIn;
79        this.refreshToken = response.refreshToken ?? this.refreshToken;
80        this.scope = response.scope ?? this.scope;
81        this.state = response.state ?? this.state;
82        this.idToken = response.idToken ?? this.idToken;
83        this.issuedAt = response.issuedAt ?? this.issuedAt ?? getCurrentTimeInSeconds();
84    }
85    getRequestConfig() {
86        return {
87            accessToken: this.accessToken,
88            idToken: this.idToken,
89            refreshToken: this.refreshToken,
90            scope: this.scope,
91            state: this.state,
92            tokenType: this.tokenType,
93            issuedAt: this.issuedAt,
94            expiresIn: this.expiresIn,
95        };
96    }
97    async refreshAsync(config, discovery) {
98        const request = new RefreshTokenRequest({
99            ...config,
100            refreshToken: this.refreshToken,
101        });
102        const response = await request.performAsync(discovery);
103        // Custom: reuse the refresh token if one wasn't returned
104        response.refreshToken = response.refreshToken ?? this.refreshToken;
105        const json = response.getRequestConfig();
106        this.applyResponseConfig(json);
107        return this;
108    }
109    shouldRefresh() {
110        // no refresh token available and token has expired
111        return !(TokenResponse.isTokenFresh(this) || !this.refreshToken);
112    }
113}
114class Request {
115    request;
116    constructor(request) {
117        this.request = request;
118    }
119    async performAsync(discovery) {
120        throw new Error('performAsync must be extended');
121    }
122    getRequestConfig() {
123        throw new Error('getRequestConfig must be extended');
124    }
125    getQueryBody() {
126        throw new Error('getQueryBody must be extended');
127    }
128}
129/**
130 * A generic token request.
131 */
132class TokenRequest extends Request {
133    grantType;
134    clientId;
135    clientSecret;
136    scopes;
137    extraParams;
138    constructor(request, grantType) {
139        super(request);
140        this.grantType = grantType;
141        this.clientId = request.clientId;
142        this.clientSecret = request.clientSecret;
143        this.extraParams = request.extraParams;
144        this.scopes = request.scopes;
145    }
146    getHeaders() {
147        const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
148        if (typeof this.clientSecret !== 'undefined') {
149            // If client secret exists, it should be converted to base64
150            // https://tools.ietf.org/html/rfc6749#section-2.3.1
151            const encodedClientId = encodeURIComponent(this.clientId);
152            const encodedClientSecret = encodeURIComponent(this.clientSecret);
153            const credentials = `${encodedClientId}:${encodedClientSecret}`;
154            const basicAuth = Base64.encodeNoWrap(credentials);
155            headers.Authorization = `Basic ${basicAuth}`;
156        }
157        return headers;
158    }
159    async performAsync(discovery) {
160        // redirect URI must not be nil
161        invariant(discovery.tokenEndpoint, `Cannot invoke \`performAsync()\` without a valid tokenEndpoint`);
162        const response = await requestAsync(discovery.tokenEndpoint, {
163            dataType: 'json',
164            method: 'POST',
165            headers: this.getHeaders(),
166            body: this.getQueryBody(),
167        });
168        if ('error' in response) {
169            throw new TokenError(response);
170        }
171        return new TokenResponse({
172            accessToken: response.access_token,
173            tokenType: response.token_type,
174            expiresIn: response.expires_in,
175            refreshToken: response.refresh_token,
176            scope: response.scope,
177            idToken: response.id_token,
178            issuedAt: response.issued_at,
179        });
180    }
181    getQueryBody() {
182        const queryBody = {
183            grant_type: this.grantType,
184        };
185        if (!this.clientSecret) {
186            // Only add the client ID if client secret is not present, otherwise pass the client id with the secret in the request body.
187            queryBody.client_id = this.clientId;
188        }
189        if (this.scopes) {
190            queryBody.scope = this.scopes.join(' ');
191        }
192        if (this.extraParams) {
193            for (const extra in this.extraParams) {
194                if (extra in this.extraParams && !(extra in queryBody)) {
195                    queryBody[extra] = this.extraParams[extra];
196                }
197            }
198        }
199        return queryBody;
200    }
201}
202/**
203 * Access token request. Exchange an authorization code for a user access token.
204 *
205 * [Section 4.1.3](https://tools.ietf.org/html/rfc6749#section-4.1.3)
206 */
207export class AccessTokenRequest extends TokenRequest {
208    code;
209    redirectUri;
210    constructor(options) {
211        invariant(options.redirectUri, `\`AccessTokenRequest\` requires a valid \`redirectUri\` (it must also match the one used in the auth request). Example: ${Platform.select({
212            web: 'https://yourwebsite.com/redirect',
213            default: 'myapp://redirect',
214        })}`);
215        invariant(options.code, `\`AccessTokenRequest\` requires a valid authorization \`code\`. This is what's received from the authorization server after an auth request.`);
216        super(options, GrantType.AuthorizationCode);
217        this.code = options.code;
218        this.redirectUri = options.redirectUri;
219    }
220    getQueryBody() {
221        const queryBody = super.getQueryBody();
222        if (this.redirectUri) {
223            queryBody.redirect_uri = this.redirectUri;
224        }
225        if (this.code) {
226            queryBody.code = this.code;
227        }
228        return queryBody;
229    }
230    getRequestConfig() {
231        return {
232            clientId: this.clientId,
233            clientSecret: this.clientSecret,
234            grantType: this.grantType,
235            code: this.code,
236            redirectUri: this.redirectUri,
237            extraParams: this.extraParams,
238            scopes: this.scopes,
239        };
240    }
241}
242/**
243 * Refresh request.
244 *
245 * [Section 6](https://tools.ietf.org/html/rfc6749#section-6)
246 */
247export class RefreshTokenRequest extends TokenRequest {
248    refreshToken;
249    constructor(options) {
250        invariant(options.refreshToken, `\`RefreshTokenRequest\` requires a valid \`refreshToken\`.`);
251        super(options, GrantType.RefreshToken);
252        this.refreshToken = options.refreshToken;
253    }
254    getQueryBody() {
255        const queryBody = super.getQueryBody();
256        if (this.refreshToken) {
257            queryBody.refresh_token = this.refreshToken;
258        }
259        return queryBody;
260    }
261    getRequestConfig() {
262        return {
263            clientId: this.clientId,
264            clientSecret: this.clientSecret,
265            grantType: this.grantType,
266            refreshToken: this.refreshToken,
267            extraParams: this.extraParams,
268            scopes: this.scopes,
269        };
270    }
271}
272/**
273 * Revocation request for a given token.
274 *
275 * [Section 2.1](https://tools.ietf.org/html/rfc7009#section-2.1)
276 */
277export class RevokeTokenRequest extends Request {
278    clientId;
279    clientSecret;
280    token;
281    tokenTypeHint;
282    constructor(request) {
283        super(request);
284        invariant(request.token, `\`RevokeTokenRequest\` requires a valid \`token\` to revoke.`);
285        this.clientId = request.clientId;
286        this.clientSecret = request.clientSecret;
287        this.token = request.token;
288        this.tokenTypeHint = request.tokenTypeHint;
289    }
290    getHeaders() {
291        const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
292        if (typeof this.clientSecret !== 'undefined' && this.clientId) {
293            // If client secret exists, it should be converted to base64
294            // https://tools.ietf.org/html/rfc6749#section-2.3.1
295            const encodedClientId = encodeURIComponent(this.clientId);
296            const encodedClientSecret = encodeURIComponent(this.clientSecret);
297            const credentials = `${encodedClientId}:${encodedClientSecret}`;
298            const basicAuth = Base64.encodeNoWrap(credentials);
299            headers.Authorization = `Basic ${basicAuth}`;
300        }
301        return headers;
302    }
303    /**
304     * Perform a token revocation request.
305     *
306     * @param discovery The `revocationEndpoint` for a provider.
307     */
308    async performAsync(discovery) {
309        invariant(discovery.revocationEndpoint, `Cannot invoke \`performAsync()\` without a valid revocationEndpoint`);
310        await requestAsync(discovery.revocationEndpoint, {
311            method: 'POST',
312            headers: this.getHeaders(),
313            body: this.getQueryBody(),
314        });
315        return true;
316    }
317    getRequestConfig() {
318        return {
319            clientId: this.clientId,
320            clientSecret: this.clientSecret,
321            token: this.token,
322            tokenTypeHint: this.tokenTypeHint,
323        };
324    }
325    getQueryBody() {
326        const queryBody = { token: this.token };
327        if (this.tokenTypeHint) {
328            queryBody.token_type_hint = this.tokenTypeHint;
329        }
330        // Include client creds https://tools.ietf.org/html/rfc6749#section-2.3.1
331        if (this.clientId) {
332            queryBody.client_id = this.clientId;
333        }
334        if (this.clientSecret) {
335            queryBody.client_secret = this.clientSecret;
336        }
337        return queryBody;
338    }
339}
340// @needsAudit
341/**
342 * Exchange an authorization code for an access token that can be used to get data from the provider.
343 *
344 * @param config Configuration used to exchange the code for a token.
345 * @param discovery The `tokenEndpoint` for a provider.
346 * @return Returns a discovery document with a valid `tokenEndpoint` URL.
347 */
348export function exchangeCodeAsync(config, discovery) {
349    const request = new AccessTokenRequest(config);
350    return request.performAsync(discovery);
351}
352// @needsAudit
353/**
354 * Refresh an access token.
355 * - If the provider didn't return a `refresh_token` then the access token may not be refreshed.
356 * - If the provider didn't return a `expires_in` then it's assumed that the token does not expire.
357 * - Determine if a token needs to be refreshed via `TokenResponse.isTokenFresh()` or `shouldRefresh()` on an instance of `TokenResponse`.
358 *
359 * @see [Section 6](https://tools.ietf.org/html/rfc6749#section-6).
360 *
361 * @param config Configuration used to refresh the given access token.
362 * @param discovery The `tokenEndpoint` for a provider.
363 * @return Returns a discovery document with a valid `tokenEndpoint` URL.
364 */
365export function refreshAsync(config, discovery) {
366    const request = new RefreshTokenRequest(config);
367    return request.performAsync(discovery);
368}
369// @needsAudit
370/**
371 * Revoke a token with a provider. This makes the token unusable, effectively requiring the user to login again.
372 *
373 * @param config Configuration used to revoke a refresh or access token.
374 * @param discovery The `revocationEndpoint` for a provider.
375 * @return Returns a discovery document with a valid `revocationEndpoint` URL. Many providers do not support this feature.
376 */
377export function revokeAsync(config, discovery) {
378    const request = new RevokeTokenRequest(config);
379    return request.performAsync(discovery);
380}
381/**
382 * Fetch generic user info from the provider's OpenID Connect `userInfoEndpoint` (if supported).
383 *
384 * @see [UserInfo](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo).
385 *
386 * @param config The `accessToken` for a user, returned from a code exchange or auth request.
387 * @param discovery The `userInfoEndpoint` for a provider.
388 */
389export function fetchUserInfoAsync(config, discovery) {
390    if (!discovery.userInfoEndpoint) {
391        throw new Error('User info endpoint is not defined in the service config discovery document');
392    }
393    return requestAsync(discovery.userInfoEndpoint, {
394        headers: {
395            'Content-Type': 'application/x-www-form-urlencoded',
396            Authorization: `Bearer ${config.accessToken}`,
397        },
398        dataType: 'json',
399        method: 'GET',
400    });
401}
402//# sourceMappingURL=TokenRequest.js.map