UNPKG

5.28 kBPlain TextView Raw
1import { CodedError, UnavailabilityError } from '@unimodules/core';
2import invariant from 'invariant';
3
4import {
5 OAuthBaseProps,
6 OAuthProps,
7 OAuthRevokeOptions,
8 OAuthServiceConfiguration,
9 TokenResponse,
10} from './AppAuth.types';
11import ExpoAppAuth from './ExpoAppAuth';
12
13export * from './AppAuth.types';
14
15function isValidServiceConfiguration(config?: OAuthServiceConfiguration): boolean {
16 return !!(
17 config &&
18 typeof config.authorizationEndpoint === 'string' &&
19 typeof config.tokenEndpoint === 'string'
20 );
21}
22
23function assertValidClientId(clientId?: string): void {
24 if (typeof clientId !== 'string' || !clientId.length) {
25 throw new CodedError(
26 'ERR_APP_AUTH_INVALID_CONFIG',
27 '`clientId` must be a string with more than 0 characters'
28 );
29 }
30}
31
32function assertValidProps({
33 issuer,
34 redirectUrl,
35 clientId,
36 serviceConfiguration,
37}: OAuthProps): void {
38 if (typeof issuer !== 'string' && !isValidServiceConfiguration(serviceConfiguration)) {
39 throw new CodedError(
40 'ERR_APP_AUTH_INVALID_CONFIG',
41 'You must provide either an `issuer` or both `authorizationEndpoint` and `tokenEndpoint`'
42 );
43 }
44 if (typeof redirectUrl !== 'string') {
45 throw new CodedError('ERR_APP_AUTH_INVALID_CONFIG', '`redirectUrl` must be a string');
46 }
47 assertValidClientId(clientId);
48}
49
50async function _executeAsync(props: OAuthProps): Promise<TokenResponse> {
51 if (!props.redirectUrl) {
52 props.redirectUrl = getDefaultOAuthRedirect();
53 }
54 assertValidProps(props);
55 return await ExpoAppAuth.executeAsync(props);
56}
57
58export function getDefaultOAuthRedirect(): string {
59 return `${ExpoAppAuth.OAuthRedirect}:/oauthredirect`;
60}
61
62export async function authAsync(props: OAuthProps): Promise<TokenResponse> {
63 if (!ExpoAppAuth.executeAsync) {
64 throw new UnavailabilityError('expo-app-auth', 'authAsync');
65 }
66 return await _executeAsync(props);
67}
68
69export async function refreshAsync(
70 props: OAuthProps,
71 refreshToken: string
72): Promise<TokenResponse> {
73 if (!ExpoAppAuth.executeAsync) {
74 throw new UnavailabilityError('expo-app-auth', 'refreshAsync');
75 }
76 if (!refreshToken) {
77 throw new CodedError('ERR_APP_AUTH_TOKEN', 'Cannot refresh with null `refreshToken`');
78 }
79 return await _executeAsync({
80 isRefresh: true,
81 refreshToken,
82 ...props,
83 });
84}
85
86/* JS Method */
87export async function revokeAsync(
88 { clientId, issuer, serviceConfiguration }: OAuthBaseProps,
89 { token, isClientIdProvided = false }: OAuthRevokeOptions
90): Promise<any> {
91 if (!token) {
92 throw new CodedError('ERR_APP_AUTH_TOKEN', 'Cannot revoke a null `token`');
93 }
94
95 assertValidClientId(clientId);
96
97 let revocationEndpoint;
98 if (serviceConfiguration && serviceConfiguration.revocationEndpoint) {
99 revocationEndpoint = serviceConfiguration.revocationEndpoint;
100 } else {
101 // For Open IDC providers only.
102 const response = await fetch(`${issuer}/.well-known/openid-configuration`);
103 const openidConfig = await response.json();
104
105 invariant(
106 openidConfig.revocation_endpoint,
107 'The OpenID config does not specify a revocation endpoint'
108 );
109
110 revocationEndpoint = openidConfig.revocation_endpoint;
111 }
112
113 const encodedClientID = encodeURIComponent(clientId);
114 const encodedToken = encodeURIComponent(token);
115 const body = `token=${encodedToken}${isClientIdProvided ? `&client_id=${encodedClientID}` : ''}`;
116 const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
117 try {
118 // https://tools.ietf.org/html/rfc7009#section-2.2
119 const results = await fetch(revocationEndpoint, {
120 method: 'POST',
121 headers,
122 body,
123 });
124
125 return results;
126 } catch (error) {
127 throw new CodedError('ERR_APP_AUTH_REVOKE_FAILED', error.message);
128 }
129}
130
131async function parseAuthRevocationResults(results: Response): Promise<any> {
132 const data = await results.json();
133 const token = results.headers['update-client-auth'];
134 // the token has been revoked successfully or the client submitted an invalid token.
135 if (results.ok) {
136 // successful op
137 return { type: 'success', status: results.status, data, token };
138 } else if (results.status == 503 && results.headers['retry-after']) {
139 // Failed op
140 const retryAfterValue = results.headers['retry-after'];
141 let retryAfter: number | undefined;
142 if (retryAfterValue) {
143 retryAfter = parseRetryTime(retryAfterValue);
144 }
145 // the client must assume the token still exists and may retry after a reasonable delay.
146 return { type: 'failed', status: results.status, data, token, retryAfter };
147 } else {
148 // Error
149 return { type: 'error', status: results.status, data, token };
150 }
151}
152
153function parseRetryTime(value: string): number {
154 // In accordance with RFC2616, Section 14.37. Timout may be of format seconds or future date time value
155 if (/^\d+$/.test(value)) {
156 return parseInt(value, 10) * 1000;
157 }
158 const retry = Date.parse(value);
159 if (isNaN(retry)) {
160 throw new CodedError(
161 'ERR_APP_AUTH_FETCH_RETRY_TIME',
162 'Cannot parse the Retry-After header value returned by the server: ' + value
163 );
164 }
165 const now = Date.now();
166 const parsedDate = new Date(retry);
167 return parsedDate.getTime() - now;
168}
169
170export const { OAuthRedirect, URLSchemes } = ExpoAppAuth;
171
\No newline at end of file