@@ -2,13 +2,16 @@ import {Log} from '../../utils/logging.js';
2
2
import fetch from 'node-fetch' ;
3
3
4
4
import {
5
+ AuthorizationError ,
6
+ AuthorizationErrorJson ,
5
7
AuthorizationNotifier ,
6
8
AuthorizationRequest ,
7
9
AuthorizationServiceConfiguration ,
8
10
BaseTokenRequestHandler ,
9
11
GRANT_TYPE_AUTHORIZATION_CODE ,
10
12
TokenRequest ,
11
13
TokenResponse ,
14
+ TokenResponseJson ,
12
15
} from '@openid/appauth' ;
13
16
import { NodeRequestor } from '@openid/appauth/built/node_support/node_requestor.js' ;
14
17
import { NodeBasedHandler } from '@openid/appauth/built/node_support/node_request_handler.js' ;
@@ -32,16 +35,16 @@ export async function authorizationCodeOAuthDance({
32
35
authConfig,
33
36
scope,
34
37
} : OAuthDanceConfig ) : Promise < TokenResponse > {
35
- if ( client_id === undefined ) {
36
- throw Error ( ) ;
37
- }
38
+ /** Requestor instance for NodeJS usage. */
38
39
const requestor = new NodeRequestor ( ) ;
39
-
40
+ /** Notifier to watch for authorization completion. */
40
41
const notifier = new AuthorizationNotifier ( ) ;
42
+ /** Handler for node based requests. */
41
43
const authorizationHandler = new NodeBasedHandler ( ) ;
42
44
43
45
authorizationHandler . setAuthorizationNotifier ( notifier ) ;
44
46
47
+ /** The authorization request. */
45
48
let request = new AuthorizationRequest ( {
46
49
client_id,
47
50
scope,
@@ -51,6 +54,7 @@ export async function authorizationCodeOAuthDance({
51
54
'access_type' : 'offline' ,
52
55
} ,
53
56
} ) ;
57
+
54
58
authorizationHandler . performAuthorizationRequest ( authConfig , request ) ;
55
59
await authorizationHandler . completeAuthorizationRequestIfPossible ( ) ;
56
60
const authorization = await authorizationHandler . authorizationPromise ;
@@ -84,10 +88,6 @@ export async function deviceCodeOAuthDance({
84
88
deviceAuthEndpoint,
85
89
scope,
86
90
} : OAuthDanceConfig ) : Promise < TokenResponse > {
87
- if ( client_id === undefined ) {
88
- throw Error ( ) ;
89
- }
90
-
91
91
// Set up and configure the authentication url to initiate the OAuth dance.
92
92
const url = new URL ( deviceAuthEndpoint ) ;
93
93
url . searchParams . append ( 'scope' , scope ) ;
@@ -101,16 +101,26 @@ export async function deviceCodeOAuthDance({
101
101
headers : { 'Accept' : 'application/json' } ,
102
102
} ) . then (
103
103
( resp ) =>
104
- resp . json ( ) as unknown as {
104
+ resp . json ( ) as Promise < {
105
105
verification_uri : string ;
106
106
verification_url : string ;
107
107
interval : number ;
108
108
user_code : string ;
109
109
expires_in : number ;
110
110
device_code : string ;
111
- } ,
111
+ } > ,
112
112
) ;
113
113
114
+ if (
115
+ isAuthorizationError ( response ) &&
116
+ ( response . error === 'invalid_client' ||
117
+ response . error === 'unsupported_grant_type' ||
118
+ response . error === 'invalid_grant' ||
119
+ response . error === 'invalid_request' )
120
+ ) {
121
+ throw new OAuthDanceError ( new AuthorizationError ( response ) . errorDescription || 'Unknown Error' ) ;
122
+ }
123
+
114
124
Log . info ( `Please visit: ${ response . verification_uri || response . verification_url } ` ) ;
115
125
Log . info ( `Enter your one time ID code: ${ response . user_code } ` ) ;
116
126
@@ -124,59 +134,36 @@ export async function deviceCodeOAuthDance({
124
134
125
135
while ( true ) {
126
136
if ( Date . now ( ) > oauthDanceTimeout ) {
127
- throw {
128
- authenticated : false ,
129
- message : 'Failed to completed OAuth authentication before the user code expired.' ,
130
- } ;
137
+ throw new OAuthDanceError (
138
+ 'Failed to completed OAuth authentication before the user code expired.' ,
139
+ ) ;
131
140
}
132
141
// Wait for the requested interval before polling, this is done before the request as it is unnecessary to
133
142
//immediately poll while the user has to perform the auth out of this flow.
134
143
await new Promise ( ( resolve ) => setTimeout ( resolve , response . interval * 1000 + pollingBackoff ) ) ;
135
144
136
- const result = await pollAuthServer (
145
+ const result = await checkStatusOfAuthServer (
137
146
authConfig . tokenEndpoint ,
138
147
response . device_code ,
139
148
client_id ,
140
149
client_secret ,
141
150
) ;
142
- if ( ! result . error ) {
143
- return {
144
- ...result ,
145
- idToken : result . id_token ,
146
- accessToken : result . access_token ,
147
- } ;
151
+
152
+ if ( ! isAuthorizationError ( result ) ) {
153
+ return new TokenResponse ( result ) ;
148
154
}
149
155
if ( result . error === 'access_denied' ) {
150
- throw {
151
- authenticated : false ,
152
- message : 'Unable to authorize, as access was denied during the OAuth flow.' ,
153
- } ;
154
- }
155
-
156
- if ( result . error === 'authorization_pending' ) {
157
- // Update messaging.
156
+ throw new OAuthDanceError ( 'Unable to authorize, as access was denied during the OAuth flow.' ) ;
158
157
}
159
158
160
159
if ( result . error === 'slow_down' ) {
161
- // Update messaging.
160
+ Log . debug ( '"slow_down" response from server, backing off polling interval by 5 seconds' ) ;
162
161
pollingBackoff += 5000 ;
163
162
}
164
-
165
- if (
166
- result . error === 'invalid_client' ||
167
- result . error === 'unsupported_grant_type' ||
168
- result . error === 'invalid_grant' ||
169
- result . error === 'invalid_request'
170
- ) {
171
- throw {
172
- authenticated : false ,
173
- message : result . errorDescription ,
174
- } ;
175
- }
176
163
}
177
164
}
178
165
179
- async function pollAuthServer (
166
+ async function checkStatusOfAuthServer (
180
167
serverUrl : string ,
181
168
deviceCode : string ,
182
169
clientId : string ,
@@ -193,7 +180,7 @@ async function pollAuthServer(
193
180
return await fetch ( url . toString ( ) , {
194
181
method : 'POST' ,
195
182
headers : { 'Accept' : 'application/json' } ,
196
- } ) . then ( ( x ) => x . json ( ) as Promise < any > ) ;
183
+ } ) . then ( ( x ) => x . json ( ) as Promise < TokenResponseJson | AuthorizationErrorJson > ) ;
197
184
}
198
185
199
186
// NOTE: the `client_secret`s are okay to be included in this code as these values are sent
@@ -242,3 +229,18 @@ export const GithubOAuthDanceConfig: OAuthDanceConfig = {
242
229
} ) ,
243
230
deviceAuthEndpoint : 'https://github.com/login/device/code' ,
244
231
} ;
232
+
233
+ class OAuthDanceError extends Error {
234
+ constructor ( message : string ) {
235
+ super ( message ) ;
236
+ }
237
+ }
238
+
239
+ function isAuthorizationError < T > (
240
+ result : T | AuthorizationErrorJson ,
241
+ ) : result is AuthorizationErrorJson {
242
+ if ( ( result as AuthorizationErrorJson ) . error !== undefined ) {
243
+ return true ;
244
+ }
245
+ return false ;
246
+ }
0 commit comments