@symbiote-native/auth-session 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +157 -2
- package/build/angular/index.d.ts +2 -0
- package/build/angular/index.js +3 -0
- package/build/angular/use-auth-request.d.ts +7 -0
- package/build/angular/use-auth-request.js +11 -0
- package/build/core/auth-request-controllers.d.ts +44 -0
- package/build/core/auth-request-controllers.js +122 -0
- package/build/core/auth-request-hooks.d.ts +67 -0
- package/build/core/auth-request-hooks.js +75 -0
- package/build/core/auth-request.d.ts +29 -0
- package/build/core/auth-request.js +161 -0
- package/build/core/auth-request.types.d.ts +46 -0
- package/build/core/auth-request.types.js +23 -0
- package/build/core/auth-session.d.ts +10 -0
- package/build/core/auth-session.js +37 -0
- package/build/core/auth-session.types.d.ts +27 -0
- package/build/core/auth-session.types.js +1 -0
- package/build/core/base64.d.ts +1 -0
- package/build/core/base64.js +28 -0
- package/build/core/discovery.d.ts +57 -0
- package/build/core/discovery.js +29 -0
- package/build/core/errors.d.ts +29 -0
- package/build/core/errors.js +74 -0
- package/build/core/fetch.d.ts +15 -0
- package/build/core/fetch.js +40 -0
- package/build/core/index.d.ts +24 -0
- package/build/core/index.js +14 -0
- package/build/core/pkce.d.ts +9 -0
- package/build/core/pkce.js +41 -0
- package/build/core/prompt-string.d.ts +2 -0
- package/build/core/prompt-string.js +5 -0
- package/build/core/providers/facebook.d.ts +16 -0
- package/build/core/providers/facebook.js +43 -0
- package/build/core/providers/google-exchange-controller.d.ts +10 -0
- package/build/core/providers/google-exchange-controller.js +73 -0
- package/build/core/providers/google.d.ts +22 -0
- package/build/core/providers/google.js +52 -0
- package/build/core/providers/provider-hook-setup.d.ts +19 -0
- package/build/core/providers/provider-hook-setup.js +78 -0
- package/build/core/providers/provider-utils.d.ts +2 -0
- package/build/core/providers/provider-utils.js +8 -0
- package/build/core/providers/provider.types.d.ts +5 -0
- package/build/core/providers/provider.types.js +1 -0
- package/build/core/query-params.d.ts +5 -0
- package/build/core/query-params.js +12 -0
- package/build/core/session-result.d.ts +6 -0
- package/build/core/session-result.js +4 -0
- package/build/core/token-request.d.ts +89 -0
- package/build/core/token-request.js +328 -0
- package/build/core/token-request.types.d.ts +52 -0
- package/build/core/token-request.types.js +14 -0
- package/build/react/index.d.ts +3 -0
- package/build/react/index.js +2 -0
- package/build/react/use-auth-request.d.ts +20 -0
- package/build/react/use-auth-request.js +52 -0
- package/build/solid/index.d.ts +2 -0
- package/build/solid/index.js +2 -0
- package/build/solid/use-auth-request.d.ts +6 -0
- package/build/solid/use-auth-request.js +10 -0
- package/build/svelte/index.d.ts +2 -0
- package/build/svelte/index.js +2 -0
- package/build/svelte/use-auth-request.svelte.d.ts +28 -0
- package/build/svelte/use-auth-request.svelte.js +11 -0
- package/build/vue/index.d.ts +2 -0
- package/build/vue/index.js +2 -0
- package/build/vue/use-auth-request.d.ts +7 -0
- package/build/vue/use-auth-request.js +12 -0
- package/build-ngc/angular/index.d.ts +2 -0
- package/build-ngc/angular/index.js +4 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/use-auth-request.d.ts +7 -0
- package/build-ngc/angular/use-auth-request.js +12 -0
- package/build-ngc/angular/use-auth-request.js.map +1 -0
- package/build-ngc/core/auth-request-controllers.d.ts +44 -0
- package/build-ngc/core/auth-request-controllers.js +123 -0
- package/build-ngc/core/auth-request-controllers.js.map +1 -0
- package/build-ngc/core/auth-request-hooks.d.ts +67 -0
- package/build-ngc/core/auth-request-hooks.js +76 -0
- package/build-ngc/core/auth-request-hooks.js.map +1 -0
- package/build-ngc/core/auth-request.d.ts +29 -0
- package/build-ngc/core/auth-request.js +162 -0
- package/build-ngc/core/auth-request.js.map +1 -0
- package/build-ngc/core/auth-request.types.d.ts +46 -0
- package/build-ngc/core/auth-request.types.js +24 -0
- package/build-ngc/core/auth-request.types.js.map +1 -0
- package/build-ngc/core/auth-session.d.ts +10 -0
- package/build-ngc/core/auth-session.js +38 -0
- package/build-ngc/core/auth-session.js.map +1 -0
- package/build-ngc/core/auth-session.types.d.ts +27 -0
- package/build-ngc/core/auth-session.types.js +2 -0
- package/build-ngc/core/auth-session.types.js.map +1 -0
- package/build-ngc/core/base64.d.ts +1 -0
- package/build-ngc/core/base64.js +29 -0
- package/build-ngc/core/base64.js.map +1 -0
- package/build-ngc/core/discovery.d.ts +57 -0
- package/build-ngc/core/discovery.js +30 -0
- package/build-ngc/core/discovery.js.map +1 -0
- package/build-ngc/core/errors.d.ts +29 -0
- package/build-ngc/core/errors.js +75 -0
- package/build-ngc/core/errors.js.map +1 -0
- package/build-ngc/core/fetch.d.ts +15 -0
- package/build-ngc/core/fetch.js +41 -0
- package/build-ngc/core/fetch.js.map +1 -0
- package/build-ngc/core/index.d.ts +24 -0
- package/build-ngc/core/index.js +15 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/pkce.d.ts +9 -0
- package/build-ngc/core/pkce.js +42 -0
- package/build-ngc/core/pkce.js.map +1 -0
- package/build-ngc/core/prompt-string.d.ts +2 -0
- package/build-ngc/core/prompt-string.js +6 -0
- package/build-ngc/core/prompt-string.js.map +1 -0
- package/build-ngc/core/providers/facebook.d.ts +16 -0
- package/build-ngc/core/providers/facebook.js +44 -0
- package/build-ngc/core/providers/facebook.js.map +1 -0
- package/build-ngc/core/providers/google-exchange-controller.d.ts +10 -0
- package/build-ngc/core/providers/google-exchange-controller.js +74 -0
- package/build-ngc/core/providers/google-exchange-controller.js.map +1 -0
- package/build-ngc/core/providers/google.d.ts +22 -0
- package/build-ngc/core/providers/google.js +53 -0
- package/build-ngc/core/providers/google.js.map +1 -0
- package/build-ngc/core/providers/provider-hook-setup.d.ts +19 -0
- package/build-ngc/core/providers/provider-hook-setup.js +79 -0
- package/build-ngc/core/providers/provider-hook-setup.js.map +1 -0
- package/build-ngc/core/providers/provider-utils.d.ts +2 -0
- package/build-ngc/core/providers/provider-utils.js +9 -0
- package/build-ngc/core/providers/provider-utils.js.map +1 -0
- package/build-ngc/core/providers/provider.types.d.ts +5 -0
- package/build-ngc/core/providers/provider.types.js +2 -0
- package/build-ngc/core/providers/provider.types.js.map +1 -0
- package/build-ngc/core/query-params.d.ts +5 -0
- package/build-ngc/core/query-params.js +13 -0
- package/build-ngc/core/query-params.js.map +1 -0
- package/build-ngc/core/session-result.d.ts +6 -0
- package/build-ngc/core/session-result.js +5 -0
- package/build-ngc/core/session-result.js.map +1 -0
- package/build-ngc/core/token-request.d.ts +89 -0
- package/build-ngc/core/token-request.js +329 -0
- package/build-ngc/core/token-request.js.map +1 -0
- package/build-ngc/core/token-request.types.d.ts +52 -0
- package/build-ngc/core/token-request.types.js +15 -0
- package/build-ngc/core/token-request.types.js.map +1 -0
- package/package.json +151 -3
- package/src/angular/index.ts +11 -0
- package/src/angular/use-auth-request.ts +30 -0
- package/src/core/auth-request-controllers.ts +233 -0
- package/src/core/auth-request-hooks.ts +342 -0
- package/src/core/auth-request.ts +199 -0
- package/src/core/auth-request.types.ts +55 -0
- package/src/core/auth-session.ts +63 -0
- package/src/core/auth-session.types.ts +29 -0
- package/src/core/base64.ts +33 -0
- package/src/core/discovery.ts +102 -0
- package/src/core/errors.ts +102 -0
- package/src/core/fetch.ts +71 -0
- package/src/core/index.ts +97 -0
- package/src/core/pkce.ts +60 -0
- package/src/core/prompt-string.ts +8 -0
- package/src/core/providers/facebook.ts +66 -0
- package/src/core/providers/google-exchange-controller.ts +117 -0
- package/src/core/providers/google.ts +81 -0
- package/src/core/providers/provider-hook-setup.ts +117 -0
- package/src/core/providers/provider-utils.ts +18 -0
- package/src/core/providers/provider.types.ts +6 -0
- package/src/core/query-params.ts +20 -0
- package/src/core/session-result.ts +15 -0
- package/src/core/token-request.ts +452 -0
- package/src/core/token-request.types.ts +60 -0
- package/src/react/index.ts +11 -0
- package/src/react/use-auth-request.ts +151 -0
- package/src/solid/index.ts +10 -0
- package/src/solid/use-auth-request.ts +24 -0
- package/src/svelte/index.ts +10 -0
- package/src/svelte/svelte-ambient.d.ts +3 -0
- package/src/svelte/svelte-compile.test-helper.ts +7 -0
- package/src/svelte/use-auth-request.svelte.ts +22 -0
- package/src/vue/index.ts +10 -0
- package/src/vue/use-auth-request.ts +38 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import invariant from 'invariant';
|
|
2
|
+
import * as Base64 from './base64';
|
|
3
|
+
import type { IDiscoveryDocument } from './discovery';
|
|
4
|
+
import { TokenError } from './errors';
|
|
5
|
+
import type { IResponseErrorConfig } from './errors';
|
|
6
|
+
import type { IFetchHeaders } from './fetch';
|
|
7
|
+
import { requestAsync } from './fetch';
|
|
8
|
+
import type {
|
|
9
|
+
IAccessTokenRequestConfig,
|
|
10
|
+
IRefreshTokenRequestConfig,
|
|
11
|
+
IRevokeTokenRequestConfig,
|
|
12
|
+
IServerTokenResponseConfig,
|
|
13
|
+
ITokenRequestConfig,
|
|
14
|
+
ITokenResponseConfig,
|
|
15
|
+
ITokenType,
|
|
16
|
+
TokenTypeHint,
|
|
17
|
+
} from './token-request.types';
|
|
18
|
+
import { GrantType } from './token-request.types';
|
|
19
|
+
|
|
20
|
+
export function getCurrentTimeInSeconds(): number {
|
|
21
|
+
return Math.floor(Date.now() / 1000);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function asTokenType(value: string | undefined): ITokenType | undefined {
|
|
25
|
+
return value === 'bearer' || value === 'mac' ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** https://tools.ietf.org/html/rfc6749#section-5.1 */
|
|
29
|
+
export class TokenResponse implements ITokenResponseConfig {
|
|
30
|
+
static isTokenFresh(
|
|
31
|
+
token: Pick<TokenResponse, 'expiresIn' | 'issuedAt'>,
|
|
32
|
+
/** -10 minutes in seconds. */
|
|
33
|
+
secondsMargin: number = 60 * 10 * -1,
|
|
34
|
+
): boolean {
|
|
35
|
+
if (!token) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (token.expiresIn) {
|
|
39
|
+
const now = getCurrentTimeInSeconds();
|
|
40
|
+
return now < token.issuedAt + token.expiresIn + secondsMargin;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static fromQueryParams(params: Record<string, string>): TokenResponse {
|
|
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: asTokenType(params.token_type),
|
|
53
|
+
expiresIn: params.expires_in ? Number(params.expires_in) : undefined,
|
|
54
|
+
issuedAt: params.issued_at ? Number(params.issued_at) : undefined,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
accessToken: string;
|
|
59
|
+
tokenType: ITokenType;
|
|
60
|
+
expiresIn?: number;
|
|
61
|
+
refreshToken?: string;
|
|
62
|
+
scope?: string;
|
|
63
|
+
state?: string;
|
|
64
|
+
idToken?: string;
|
|
65
|
+
issuedAt: number;
|
|
66
|
+
/** Unprocessed token response, for fields outside RFC 6749. */
|
|
67
|
+
rawResponse?: unknown;
|
|
68
|
+
|
|
69
|
+
constructor(response: ITokenResponseConfig, rawResponse?: unknown) {
|
|
70
|
+
this.rawResponse = rawResponse;
|
|
71
|
+
this.accessToken = response.accessToken;
|
|
72
|
+
this.tokenType = response.tokenType ?? 'bearer';
|
|
73
|
+
this.expiresIn = response.expiresIn;
|
|
74
|
+
this.refreshToken = response.refreshToken;
|
|
75
|
+
this.scope = response.scope;
|
|
76
|
+
this.state = response.state;
|
|
77
|
+
this.idToken = response.idToken;
|
|
78
|
+
this.issuedAt = response.issuedAt ?? getCurrentTimeInSeconds();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private applyResponseConfig(response: ITokenResponseConfig): void {
|
|
82
|
+
this.accessToken = response.accessToken ?? this.accessToken;
|
|
83
|
+
this.tokenType = response.tokenType ?? this.tokenType ?? 'bearer';
|
|
84
|
+
this.expiresIn = response.expiresIn ?? this.expiresIn;
|
|
85
|
+
this.refreshToken = response.refreshToken ?? this.refreshToken;
|
|
86
|
+
this.scope = response.scope ?? this.scope;
|
|
87
|
+
this.state = response.state ?? this.state;
|
|
88
|
+
this.idToken = response.idToken ?? this.idToken;
|
|
89
|
+
this.issuedAt =
|
|
90
|
+
response.issuedAt ?? this.issuedAt ?? getCurrentTimeInSeconds();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getRequestConfig(): ITokenResponseConfig {
|
|
94
|
+
return {
|
|
95
|
+
accessToken: this.accessToken,
|
|
96
|
+
idToken: this.idToken,
|
|
97
|
+
refreshToken: this.refreshToken,
|
|
98
|
+
scope: this.scope,
|
|
99
|
+
state: this.state,
|
|
100
|
+
tokenType: this.tokenType,
|
|
101
|
+
issuedAt: this.issuedAt,
|
|
102
|
+
expiresIn: this.expiresIn,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async refreshAsync(
|
|
107
|
+
config: Omit<ITokenRequestConfig, 'grantType' | 'refreshToken'>,
|
|
108
|
+
discovery: Pick<IDiscoveryDocument, 'tokenEndpoint'>,
|
|
109
|
+
): Promise<TokenResponse> {
|
|
110
|
+
const request = new RefreshTokenRequest({
|
|
111
|
+
...config,
|
|
112
|
+
refreshToken: this.refreshToken,
|
|
113
|
+
});
|
|
114
|
+
const response = await request.performAsync(discovery);
|
|
115
|
+
response.refreshToken = response.refreshToken ?? this.refreshToken;
|
|
116
|
+
this.applyResponseConfig(response.getRequestConfig());
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
shouldRefresh(): boolean {
|
|
121
|
+
return !(TokenResponse.isTokenFresh(this) || !this.refreshToken);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sanitizeExtraHeaders(
|
|
126
|
+
extra: Record<string, string> | undefined,
|
|
127
|
+
hasClientSecret: boolean,
|
|
128
|
+
): Record<string, string> | undefined {
|
|
129
|
+
if (!extra) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const extraHeaders = { ...extra };
|
|
133
|
+
delete extraHeaders['Content-Type'];
|
|
134
|
+
delete extraHeaders['content-type'];
|
|
135
|
+
if (hasClientSecret) {
|
|
136
|
+
delete extraHeaders.authorization;
|
|
137
|
+
delete extraHeaders.Authorization;
|
|
138
|
+
}
|
|
139
|
+
return extraHeaders;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function encodeBasicAuth(clientId: string, clientSecret: string): string {
|
|
143
|
+
const credentials = `${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`;
|
|
144
|
+
return Base64.encodeNoWrap(credentials);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
abstract class BaseRequest<TConfig, TResult> {
|
|
148
|
+
constructor(protected request: TConfig) {}
|
|
149
|
+
abstract performAsync(discovery: IDiscoveryDocument): Promise<TResult>;
|
|
150
|
+
abstract getRequestConfig(): TConfig;
|
|
151
|
+
abstract getQueryBody(): Record<string, string>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A generic token request. */
|
|
155
|
+
export class TokenRequest<T extends ITokenRequestConfig>
|
|
156
|
+
extends BaseRequest<T, TokenResponse>
|
|
157
|
+
implements ITokenRequestConfig
|
|
158
|
+
{
|
|
159
|
+
readonly clientId: string;
|
|
160
|
+
readonly clientSecret?: string;
|
|
161
|
+
readonly scopes?: string[];
|
|
162
|
+
readonly extraParams?: Record<string, string>;
|
|
163
|
+
readonly extraHeaders?: Record<string, string>;
|
|
164
|
+
|
|
165
|
+
constructor(
|
|
166
|
+
request: T,
|
|
167
|
+
public grantType: GrantType,
|
|
168
|
+
) {
|
|
169
|
+
super(request);
|
|
170
|
+
this.clientId = request.clientId;
|
|
171
|
+
this.clientSecret = request.clientSecret;
|
|
172
|
+
this.extraParams = request.extraParams;
|
|
173
|
+
this.scopes = request.scopes;
|
|
174
|
+
this.extraHeaders = sanitizeExtraHeaders(
|
|
175
|
+
request.extraHeaders,
|
|
176
|
+
typeof request.clientSecret !== 'undefined',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getHeaders(): IFetchHeaders {
|
|
181
|
+
const headers: IFetchHeaders = {
|
|
182
|
+
...this.extraHeaders,
|
|
183
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
184
|
+
};
|
|
185
|
+
if (typeof this.clientSecret !== 'undefined') {
|
|
186
|
+
headers.Authorization = `Basic ${encodeBasicAuth(this.clientId, this.clientSecret)}`;
|
|
187
|
+
}
|
|
188
|
+
return headers;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async performAsync(
|
|
192
|
+
discovery: Pick<IDiscoveryDocument, 'tokenEndpoint'>,
|
|
193
|
+
): Promise<TokenResponse> {
|
|
194
|
+
invariant(
|
|
195
|
+
discovery.tokenEndpoint,
|
|
196
|
+
'Cannot invoke `performAsync()` without a valid tokenEndpoint',
|
|
197
|
+
);
|
|
198
|
+
const response = await requestAsync<
|
|
199
|
+
IServerTokenResponseConfig | IResponseErrorConfig
|
|
200
|
+
>(discovery.tokenEndpoint, {
|
|
201
|
+
dataType: 'json',
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: this.getHeaders(),
|
|
204
|
+
body: this.getQueryBody(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if ('error' in response) {
|
|
208
|
+
throw new TokenError(response);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return new TokenResponse(
|
|
212
|
+
{
|
|
213
|
+
accessToken: response.access_token,
|
|
214
|
+
tokenType: response.token_type,
|
|
215
|
+
expiresIn: response.expires_in,
|
|
216
|
+
refreshToken: response.refresh_token,
|
|
217
|
+
scope: response.scope,
|
|
218
|
+
idToken: response.id_token,
|
|
219
|
+
issuedAt: response.issued_at,
|
|
220
|
+
},
|
|
221
|
+
response,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
getRequestConfig(): T {
|
|
226
|
+
return this.request;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
getQueryBody(): Record<string, string> {
|
|
230
|
+
const queryBody: Record<string, string> = { grant_type: this.grantType };
|
|
231
|
+
if (!this.clientSecret) {
|
|
232
|
+
queryBody.client_id = this.clientId;
|
|
233
|
+
}
|
|
234
|
+
if (this.scopes) {
|
|
235
|
+
queryBody.scope = this.scopes.join(' ');
|
|
236
|
+
}
|
|
237
|
+
for (const extra in this.extraParams) {
|
|
238
|
+
const param = this.extraParams[extra];
|
|
239
|
+
if (param != null && !(extra in queryBody)) {
|
|
240
|
+
queryBody[extra] = param;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return queryBody;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** https://tools.ietf.org/html/rfc6749#section-4.1.3 */
|
|
248
|
+
export class AccessTokenRequest
|
|
249
|
+
extends TokenRequest<IAccessTokenRequestConfig>
|
|
250
|
+
implements IAccessTokenRequestConfig
|
|
251
|
+
{
|
|
252
|
+
readonly code: string;
|
|
253
|
+
readonly redirectUri: string;
|
|
254
|
+
|
|
255
|
+
constructor(options: IAccessTokenRequestConfig) {
|
|
256
|
+
invariant(
|
|
257
|
+
options.redirectUri,
|
|
258
|
+
'`AccessTokenRequest` requires a valid `redirectUri`.',
|
|
259
|
+
);
|
|
260
|
+
invariant(
|
|
261
|
+
options.code,
|
|
262
|
+
'`AccessTokenRequest` requires a valid authorization `code`.',
|
|
263
|
+
);
|
|
264
|
+
super(options, GrantType.AuthorizationCode);
|
|
265
|
+
this.code = options.code;
|
|
266
|
+
this.redirectUri = options.redirectUri;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
override getQueryBody(): Record<string, string> {
|
|
270
|
+
return {
|
|
271
|
+
...super.getQueryBody(),
|
|
272
|
+
redirect_uri: this.redirectUri,
|
|
273
|
+
code: this.code,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
override getRequestConfig(): IAccessTokenRequestConfig & {
|
|
278
|
+
grantType: GrantType;
|
|
279
|
+
} {
|
|
280
|
+
return {
|
|
281
|
+
clientId: this.clientId,
|
|
282
|
+
clientSecret: this.clientSecret,
|
|
283
|
+
grantType: this.grantType,
|
|
284
|
+
code: this.code,
|
|
285
|
+
redirectUri: this.redirectUri,
|
|
286
|
+
extraParams: this.extraParams,
|
|
287
|
+
extraHeaders: this.extraHeaders,
|
|
288
|
+
scopes: this.scopes,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** https://tools.ietf.org/html/rfc6749#section-6 */
|
|
294
|
+
export class RefreshTokenRequest
|
|
295
|
+
extends TokenRequest<IRefreshTokenRequestConfig>
|
|
296
|
+
implements IRefreshTokenRequestConfig
|
|
297
|
+
{
|
|
298
|
+
readonly refreshToken?: string;
|
|
299
|
+
|
|
300
|
+
constructor(options: IRefreshTokenRequestConfig) {
|
|
301
|
+
invariant(
|
|
302
|
+
options.refreshToken,
|
|
303
|
+
'`RefreshTokenRequest` requires a valid `refreshToken`.',
|
|
304
|
+
);
|
|
305
|
+
super(options, GrantType.RefreshToken);
|
|
306
|
+
this.refreshToken = options.refreshToken;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
override getQueryBody(): Record<string, string> {
|
|
310
|
+
const queryBody = super.getQueryBody();
|
|
311
|
+
if (this.refreshToken) {
|
|
312
|
+
queryBody.refresh_token = this.refreshToken;
|
|
313
|
+
}
|
|
314
|
+
return queryBody;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
override getRequestConfig(): IRefreshTokenRequestConfig & {
|
|
318
|
+
grantType: GrantType;
|
|
319
|
+
} {
|
|
320
|
+
return {
|
|
321
|
+
clientId: this.clientId,
|
|
322
|
+
clientSecret: this.clientSecret,
|
|
323
|
+
grantType: this.grantType,
|
|
324
|
+
refreshToken: this.refreshToken,
|
|
325
|
+
extraParams: this.extraParams,
|
|
326
|
+
extraHeaders: this.extraHeaders,
|
|
327
|
+
scopes: this.scopes,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** https://tools.ietf.org/html/rfc7009#section-2.1 */
|
|
333
|
+
export class RevokeTokenRequest
|
|
334
|
+
extends BaseRequest<IRevokeTokenRequestConfig, boolean>
|
|
335
|
+
implements IRevokeTokenRequestConfig
|
|
336
|
+
{
|
|
337
|
+
readonly clientId?: string;
|
|
338
|
+
readonly clientSecret?: string;
|
|
339
|
+
readonly token: string;
|
|
340
|
+
readonly tokenTypeHint?: TokenTypeHint;
|
|
341
|
+
readonly extraHeaders?: Record<string, string>;
|
|
342
|
+
|
|
343
|
+
constructor(request: IRevokeTokenRequestConfig) {
|
|
344
|
+
super(request);
|
|
345
|
+
invariant(
|
|
346
|
+
request.token,
|
|
347
|
+
'`RevokeTokenRequest` requires a valid `token` to revoke.',
|
|
348
|
+
);
|
|
349
|
+
this.clientId = request.clientId;
|
|
350
|
+
this.clientSecret = request.clientSecret;
|
|
351
|
+
this.token = request.token;
|
|
352
|
+
this.tokenTypeHint = request.tokenTypeHint;
|
|
353
|
+
this.extraHeaders = sanitizeExtraHeaders(
|
|
354
|
+
request.extraHeaders,
|
|
355
|
+
typeof request.clientSecret !== 'undefined',
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
getHeaders(): IFetchHeaders {
|
|
360
|
+
const headers: IFetchHeaders = {
|
|
361
|
+
...this.extraHeaders,
|
|
362
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
363
|
+
};
|
|
364
|
+
if (typeof this.clientSecret !== 'undefined' && this.clientId) {
|
|
365
|
+
headers.Authorization = `Basic ${encodeBasicAuth(this.clientId, this.clientSecret)}`;
|
|
366
|
+
}
|
|
367
|
+
return headers;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async performAsync(
|
|
371
|
+
discovery: Pick<IDiscoveryDocument, 'revocationEndpoint'>,
|
|
372
|
+
): Promise<boolean> {
|
|
373
|
+
invariant(
|
|
374
|
+
discovery.revocationEndpoint,
|
|
375
|
+
'Cannot invoke `performAsync()` without a valid revocationEndpoint',
|
|
376
|
+
);
|
|
377
|
+
await requestAsync(discovery.revocationEndpoint, {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
headers: this.getHeaders(),
|
|
380
|
+
body: this.getQueryBody(),
|
|
381
|
+
});
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
getRequestConfig(): IRevokeTokenRequestConfig {
|
|
386
|
+
return {
|
|
387
|
+
clientId: this.clientId,
|
|
388
|
+
clientSecret: this.clientSecret,
|
|
389
|
+
token: this.token,
|
|
390
|
+
tokenTypeHint: this.tokenTypeHint,
|
|
391
|
+
extraHeaders: this.extraHeaders,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
getQueryBody(): Record<string, string> {
|
|
396
|
+
const queryBody: Record<string, string> = { token: this.token };
|
|
397
|
+
if (this.tokenTypeHint) {
|
|
398
|
+
queryBody.token_type_hint = this.tokenTypeHint;
|
|
399
|
+
}
|
|
400
|
+
if (this.clientId) {
|
|
401
|
+
queryBody.client_id = this.clientId;
|
|
402
|
+
}
|
|
403
|
+
if (this.clientSecret) {
|
|
404
|
+
queryBody.client_secret = this.clientSecret;
|
|
405
|
+
}
|
|
406
|
+
return queryBody;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Exchange an authorization code for an access token. */
|
|
411
|
+
export function exchangeCodeAsync(
|
|
412
|
+
config: IAccessTokenRequestConfig,
|
|
413
|
+
discovery: Pick<IDiscoveryDocument, 'tokenEndpoint'>,
|
|
414
|
+
): Promise<TokenResponse> {
|
|
415
|
+
return new AccessTokenRequest(config).performAsync(discovery);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Refresh an access token. See `TokenResponse.isTokenFresh()`/`shouldRefresh()`. */
|
|
419
|
+
export function refreshAsync(
|
|
420
|
+
config: IRefreshTokenRequestConfig,
|
|
421
|
+
discovery: Pick<IDiscoveryDocument, 'tokenEndpoint'>,
|
|
422
|
+
): Promise<TokenResponse> {
|
|
423
|
+
return new RefreshTokenRequest(config).performAsync(discovery);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Revoke a token, effectively requiring the user to sign in again. */
|
|
427
|
+
export function revokeAsync(
|
|
428
|
+
config: IRevokeTokenRequestConfig,
|
|
429
|
+
discovery: Pick<IDiscoveryDocument, 'revocationEndpoint'>,
|
|
430
|
+
): Promise<boolean> {
|
|
431
|
+
return new RevokeTokenRequest(config).performAsync(discovery);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Fetch generic user info from the provider's OpenID Connect `userInfoEndpoint`, if supported. */
|
|
435
|
+
export function fetchUserInfoAsync(
|
|
436
|
+
config: Pick<TokenResponse, 'accessToken'>,
|
|
437
|
+
discovery: Pick<IDiscoveryDocument, 'userInfoEndpoint'>,
|
|
438
|
+
): Promise<Record<string, unknown>> {
|
|
439
|
+
if (!discovery.userInfoEndpoint) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
'User info endpoint is not defined in the service config discovery document',
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return requestAsync<Record<string, unknown>>(discovery.userInfoEndpoint, {
|
|
445
|
+
headers: {
|
|
446
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
447
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
448
|
+
},
|
|
449
|
+
dataType: 'json',
|
|
450
|
+
method: 'GET',
|
|
451
|
+
});
|
|
452
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** https://tools.ietf.org/html/rfc6749#section-7.1 */
|
|
2
|
+
export type ITokenType = 'bearer' | 'mac';
|
|
3
|
+
|
|
4
|
+
/** A hint about the type of the token submitted for revocation. */
|
|
5
|
+
export enum TokenTypeHint {
|
|
6
|
+
AccessToken = 'access_token',
|
|
7
|
+
RefreshToken = 'refresh_token',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ITokenRequestConfig = {
|
|
11
|
+
clientId: string;
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
extraParams?: Record<string, string>;
|
|
14
|
+
extraHeaders?: Record<string, string>;
|
|
15
|
+
scopes?: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type IAccessTokenRequestConfig = ITokenRequestConfig & {
|
|
19
|
+
code: string;
|
|
20
|
+
/** Must match the `redirectUri` used in the `AuthRequest`. */
|
|
21
|
+
redirectUri: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type IRefreshTokenRequestConfig = ITokenRequestConfig & {
|
|
25
|
+
refreshToken?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type IRevokeTokenRequestConfig = Partial<ITokenRequestConfig> & {
|
|
29
|
+
token: string;
|
|
30
|
+
tokenTypeHint?: TokenTypeHint;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** https://tools.ietf.org/html/rfc6749#appendix-A.10 */
|
|
34
|
+
export enum GrantType {
|
|
35
|
+
AuthorizationCode = 'authorization_code',
|
|
36
|
+
Implicit = 'implicit',
|
|
37
|
+
RefreshToken = 'refresh_token',
|
|
38
|
+
ClientCredentials = 'client_credentials',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type IServerTokenResponseConfig = {
|
|
42
|
+
access_token: string;
|
|
43
|
+
token_type?: ITokenType;
|
|
44
|
+
expires_in?: number;
|
|
45
|
+
refresh_token?: string;
|
|
46
|
+
scope?: string;
|
|
47
|
+
id_token?: string;
|
|
48
|
+
issued_at?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ITokenResponseConfig = {
|
|
52
|
+
accessToken: string;
|
|
53
|
+
tokenType?: ITokenType;
|
|
54
|
+
expiresIn?: number;
|
|
55
|
+
refreshToken?: string;
|
|
56
|
+
scope?: string;
|
|
57
|
+
state?: string;
|
|
58
|
+
idToken?: string;
|
|
59
|
+
issuedAt?: number;
|
|
60
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from '../core';
|
|
2
|
+
export {
|
|
3
|
+
useAuthRequest,
|
|
4
|
+
useAuthRequestResult,
|
|
5
|
+
useAutoDiscovery,
|
|
6
|
+
useFacebookAuthRequest,
|
|
7
|
+
useGoogleAuthRequest,
|
|
8
|
+
useGoogleIdTokenAuthRequest,
|
|
9
|
+
useLoadedAuthRequest,
|
|
10
|
+
} from './use-auth-request';
|
|
11
|
+
export type { IUseAuthRequestResult } from './use-auth-request';
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// React twins of `expo-auth-session`'s auth request hooks. The load, prompt and code-exchange
|
|
2
|
+
// logic is the shared controllers in `core/`, React only supplies the lifecycle
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createEventValueHook,
|
|
6
|
+
createResourceHook,
|
|
7
|
+
} from '@symbiote-native/react';
|
|
8
|
+
import {
|
|
9
|
+
AUTH_CELL_CHANGE,
|
|
10
|
+
AuthRequest,
|
|
11
|
+
FacebookAuthRequest,
|
|
12
|
+
GoogleAuthRequest,
|
|
13
|
+
createAutoDiscoveryController,
|
|
14
|
+
createGoogleExchangeController,
|
|
15
|
+
createLoadedRequestController,
|
|
16
|
+
createRequestResultController,
|
|
17
|
+
facebookDiscovery,
|
|
18
|
+
googleDiscovery,
|
|
19
|
+
resolveFacebookRequestSetup,
|
|
20
|
+
resolveGoogleRequestSetup,
|
|
21
|
+
toGoogleIdTokenConfig,
|
|
22
|
+
} from '../core';
|
|
23
|
+
import type {
|
|
24
|
+
IAuthCell,
|
|
25
|
+
IAuthRequestClass,
|
|
26
|
+
IAuthRequestConfig,
|
|
27
|
+
IAuthRequestPromptOptions,
|
|
28
|
+
IAuthSessionRedirectUriOptions,
|
|
29
|
+
IAuthSessionResult,
|
|
30
|
+
IDiscoveryDocument,
|
|
31
|
+
IFacebookAuthRequestConfig,
|
|
32
|
+
IGoogleAuthRequestConfig,
|
|
33
|
+
IIssuerOrDiscovery,
|
|
34
|
+
IPromptMethod,
|
|
35
|
+
} from '../core';
|
|
36
|
+
|
|
37
|
+
type IDiscoveryValue = IDiscoveryDocument | null;
|
|
38
|
+
type IResultValue = IAuthSessionResult | null;
|
|
39
|
+
|
|
40
|
+
export type IUseAuthRequestResult<TRequest extends AuthRequest = AuthRequest> =
|
|
41
|
+
readonly [TRequest | null, IResultValue, IPromptMethod];
|
|
42
|
+
|
|
43
|
+
function readAuthCell<T>(cell: IAuthCell<T>): T {
|
|
44
|
+
return cell.getSnapshot();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const useDiscoveryCell = createResourceHook(createAutoDiscoveryController);
|
|
48
|
+
const useLoadedCell = createResourceHook(() =>
|
|
49
|
+
createLoadedRequestController<AuthRequest>(),
|
|
50
|
+
);
|
|
51
|
+
const useResultHolder = createResourceHook(createRequestResultController);
|
|
52
|
+
const useExchangeCell = createResourceHook(createGoogleExchangeController);
|
|
53
|
+
const useDiscoveryValue = createEventValueHook<
|
|
54
|
+
IAuthCell<IDiscoveryValue>,
|
|
55
|
+
IDiscoveryValue,
|
|
56
|
+
typeof AUTH_CELL_CHANGE
|
|
57
|
+
>(AUTH_CELL_CHANGE, readAuthCell);
|
|
58
|
+
const useRequestValue = createEventValueHook<
|
|
59
|
+
IAuthCell<AuthRequest | null>,
|
|
60
|
+
AuthRequest | null,
|
|
61
|
+
typeof AUTH_CELL_CHANGE
|
|
62
|
+
>(AUTH_CELL_CHANGE, readAuthCell);
|
|
63
|
+
const useResultValue = createEventValueHook<
|
|
64
|
+
IAuthCell<IResultValue>,
|
|
65
|
+
IResultValue,
|
|
66
|
+
typeof AUTH_CELL_CHANGE
|
|
67
|
+
>(AUTH_CELL_CHANGE, readAuthCell);
|
|
68
|
+
|
|
69
|
+
/** React twin of `useAutoDiscovery`, `null` until the discovery document has loaded */
|
|
70
|
+
export function useAutoDiscovery(
|
|
71
|
+
issuerOrDiscovery: IIssuerOrDiscovery,
|
|
72
|
+
): IDiscoveryValue {
|
|
73
|
+
return useDiscoveryValue(useDiscoveryCell(issuerOrDiscovery));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** React twin of `useLoadedAuthRequest`, `null` until the request url has loaded */
|
|
77
|
+
export function useLoadedAuthRequest(
|
|
78
|
+
config: IAuthRequestConfig,
|
|
79
|
+
discovery: IDiscoveryValue,
|
|
80
|
+
RequestClass: IAuthRequestClass<AuthRequest>,
|
|
81
|
+
): AuthRequest | null {
|
|
82
|
+
return useRequestValue(useLoadedCell(config, discovery, RequestClass));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** React twin of `useAuthRequestResult`, the result and the prompt method */
|
|
86
|
+
export function useAuthRequestResult(
|
|
87
|
+
request: AuthRequest | null,
|
|
88
|
+
discovery: IDiscoveryValue,
|
|
89
|
+
customOptions: IAuthRequestPromptOptions = {},
|
|
90
|
+
): readonly [IResultValue, IPromptMethod] {
|
|
91
|
+
const holder = useResultHolder(request, discovery, customOptions);
|
|
92
|
+
return [useResultValue(holder.result), holder.promptAsync];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** React twin of `useAuthRequest`: the loaded request, the result and the prompt method */
|
|
96
|
+
export function useAuthRequest(
|
|
97
|
+
config: IAuthRequestConfig,
|
|
98
|
+
discovery: IDiscoveryValue,
|
|
99
|
+
): IUseAuthRequestResult {
|
|
100
|
+
const request = useLoadedAuthRequest(config, discovery, AuthRequest);
|
|
101
|
+
const [result, promptAsync] = useAuthRequestResult(request, discovery);
|
|
102
|
+
return [request, result, promptAsync];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** React twin of Google's `useAuthRequest`, exchanges the code for a token by default */
|
|
106
|
+
export function useGoogleAuthRequest(
|
|
107
|
+
config: Partial<IGoogleAuthRequestConfig> = {},
|
|
108
|
+
redirectUriOptions: Partial<IAuthSessionRedirectUriOptions> = {},
|
|
109
|
+
): IUseAuthRequestResult<GoogleAuthRequest> {
|
|
110
|
+
const { requestConfig, exchange } = resolveGoogleRequestSetup(
|
|
111
|
+
config,
|
|
112
|
+
redirectUriOptions,
|
|
113
|
+
);
|
|
114
|
+
const request = useLoadedAuthRequest(
|
|
115
|
+
requestConfig,
|
|
116
|
+
googleDiscovery,
|
|
117
|
+
GoogleAuthRequest,
|
|
118
|
+
);
|
|
119
|
+
const [result, promptAsync] = useAuthRequestResult(request, googleDiscovery);
|
|
120
|
+
const fullResult = useResultValue(useExchangeCell(result, request, exchange));
|
|
121
|
+
return [request, fullResult, promptAsync];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** React twin of Google's `useIdTokenAuthRequest`, the id token is in `params.id_token` */
|
|
125
|
+
export function useGoogleIdTokenAuthRequest(
|
|
126
|
+
config: Partial<IGoogleAuthRequestConfig>,
|
|
127
|
+
redirectUriOptions: Partial<IAuthSessionRedirectUriOptions> = {},
|
|
128
|
+
): IUseAuthRequestResult<GoogleAuthRequest> {
|
|
129
|
+
return useGoogleAuthRequest(
|
|
130
|
+
toGoogleIdTokenConfig(config),
|
|
131
|
+
redirectUriOptions,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** React twin of Facebook's `useAuthRequest` */
|
|
136
|
+
export function useFacebookAuthRequest(
|
|
137
|
+
config: Partial<IFacebookAuthRequestConfig> = {},
|
|
138
|
+
redirectUriOptions: Partial<IAuthSessionRedirectUriOptions> = {},
|
|
139
|
+
): IUseAuthRequestResult<FacebookAuthRequest> {
|
|
140
|
+
const requestConfig = resolveFacebookRequestSetup(config, redirectUriOptions);
|
|
141
|
+
const request = useLoadedAuthRequest(
|
|
142
|
+
requestConfig,
|
|
143
|
+
facebookDiscovery,
|
|
144
|
+
FacebookAuthRequest,
|
|
145
|
+
);
|
|
146
|
+
const [result, promptAsync] = useAuthRequestResult(
|
|
147
|
+
request,
|
|
148
|
+
facebookDiscovery,
|
|
149
|
+
);
|
|
150
|
+
return [request, result, promptAsync];
|
|
151
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Solid twins of `expo-auth-session`'s auth request hooks: accessor arguments, accessor results
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createEventValueHook,
|
|
5
|
+
createResourceHook,
|
|
6
|
+
} from '@symbiote-native/solid';
|
|
7
|
+
import { GETTER_ARGS, createAuthRequestHooks } from '../core';
|
|
8
|
+
import type { IGetterKind } from '../core';
|
|
9
|
+
|
|
10
|
+
export const {
|
|
11
|
+
useAuthRequest,
|
|
12
|
+
useAuthRequestResult,
|
|
13
|
+
useAutoDiscovery,
|
|
14
|
+
useFacebookAuthRequest,
|
|
15
|
+
useGoogleAuthRequest,
|
|
16
|
+
useGoogleIdTokenAuthRequest,
|
|
17
|
+
useLoadedAuthRequest,
|
|
18
|
+
} = createAuthRequestHooks<IGetterKind, IGetterKind, IGetterKind>({
|
|
19
|
+
createResourceHook,
|
|
20
|
+
createEventValueHook,
|
|
21
|
+
readResource: box => box(),
|
|
22
|
+
readValue: box => box(),
|
|
23
|
+
...GETTER_ARGS,
|
|
24
|
+
});
|