@@ -12,12 +12,38 @@ import { StartDeviceAuthorizationResponse } from 'aws-sdk/clients/ssooidc'
12
12
13
13
const CLIENT_REGISTRATION_TYPE = 'public'
14
14
const CLIENT_NAME = 'aws-toolkit-vscode'
15
- // According to Spec 'SSO Login Token Flow' the grant type must be the following string
15
+ // Grant type specified by the 'SSO Login Token Flow' spec.
16
16
const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code'
17
- // Used to convert seconds to milliseconds
18
- const MILLISECONDS_PER_SECOND = 1000
17
+ const MS_PER_SECOND = 1000
19
18
const BACKOFF_DELAY_MS = 5000
20
19
20
+ /**
21
+ * SSO flow (RFC: https://tools.ietf.org/html/rfc8628)
22
+ * 1. Get a client id (SSO-OIDC identifier, formatted per RFC6749).
23
+ * - Toolkit code: `registerClient()`
24
+ * - RETURNS:
25
+ * - ClientSecret
26
+ * - ClientId
27
+ * - ClientSecretExpiresAt
28
+ * - Client registration is valid for potentially months and creates state
29
+ * server-side, so the client SHOULD cache them to disk.
30
+ * 2. Start device authorization.
31
+ * - Toolkit code: `authorizeClient()`
32
+ * - StartDeviceAuthorization(clientSecret, clientId, startUrl)
33
+ * - RETURNS (RFC: https://tools.ietf.org/html/rfc8628#section-3.2):
34
+ * - DeviceCode : Device verification code
35
+ * - UserCode : User verification code
36
+ * - VerificationUri : User verification URI on the authorization server
37
+ * - VerificationUriComplete: User verification URI including the `user_code`
38
+ * - ExpiresIn : Lifetime (seconds) of `device_code` and `user_code`
39
+ * - Interval : Minimum time (seconds) the client SHOULD wait between polling intervals.
40
+ * 3. Poll for the access token.
41
+ * - Toolkit code: `pollForToken()`
42
+ * - Call CreateToken() in a loop.
43
+ * - RETURNS:
44
+ * - AccessToken
45
+ * - ExpiresIn
46
+ */
21
47
export class SsoAccessTokenProvider {
22
48
public constructor (
23
49
private ssoRegion : string ,
@@ -31,8 +57,11 @@ export class SsoAccessTokenProvider {
31
57
if ( accessToken ) {
32
58
return accessToken
33
59
}
60
+ // SSO step 1
34
61
const registration = await this . registerClient ( )
62
+ // SSO step 2
35
63
const authorization = await this . authorizeClient ( registration )
64
+ // SSO step 3
36
65
const token = await this . pollForToken ( registration , authorization )
37
66
this . cache . saveAccessToken ( this . ssoUrl , token )
38
67
return token
@@ -42,30 +71,28 @@ export class SsoAccessTokenProvider {
42
71
this . cache . invalidateAccessToken ( this . ssoUrl )
43
72
}
44
73
74
+ /**
75
+ * SSO step 3: poll for the access token.
76
+ */
45
77
private async pollForToken (
46
78
registration : SsoClientRegistration ,
47
- authorization : StartDeviceAuthorizationResponse
79
+ authz : StartDeviceAuthorizationResponse
48
80
) : Promise < SsoAccessToken > {
49
- // Calculate the device code expiration in milliseconds
50
- const deviceCodeExpiration = this . currentTimePlusSecondsInMs ( authorization . expiresIn ! )
51
-
52
- getLogger ( ) . info (
53
- `To complete authentication for this SSO account, please continue to this SSO portal:${ authorization . verificationUriComplete } `
54
- )
55
-
56
- // The retry interval converted to milliseconds
57
- let retryInterval : number
58
- if ( authorization . interval != undefined && authorization . interval ! > 0 ) {
59
- retryInterval = authorization . interval ! * MILLISECONDS_PER_SECOND
60
- } else {
61
- retryInterval = BACKOFF_DELAY_MS
62
- }
81
+ // Device code expiration in milliseconds.
82
+ const deviceCodeExpiration = this . currentTimePlusSecondsInMs ( authz . expiresIn ! )
83
+ const deviceCodeExpiredMsg = 'SSO: device code expired, login flow must be reinitiated'
84
+
85
+ getLogger ( ) . info ( `SSO: to complete sign-in, visit: ${ authz . verificationUriComplete } ` )
86
+
87
+ /** Retry interval in milliseconds. */
88
+ let retryInterval =
89
+ authz . interval !== undefined && authz . interval ! > 0 ? authz . interval ! * MS_PER_SECOND : BACKOFF_DELAY_MS
63
90
64
91
const createTokenParams = {
65
92
clientId : registration . clientId ,
66
93
clientSecret : registration . clientSecret ,
67
94
grantType : GRANT_TYPE ,
68
- deviceCode : authorization . deviceCode ! ,
95
+ deviceCode : authz . deviceCode ! ,
69
96
}
70
97
71
98
while ( true ) {
@@ -82,23 +109,26 @@ export class SsoAccessTokenProvider {
82
109
if ( err . code === 'SlowDownException' ) {
83
110
retryInterval += BACKOFF_DELAY_MS
84
111
} else if ( err . code === 'AuthorizationPendingException' ) {
85
- // do nothing, wait the interval and try again
112
+ // Do nothing, try again after the interval.
86
113
} else if ( err . code === 'ExpiredTokenException' ) {
87
- throw Error ( `Device code has expired while polling for SSO token, login flow must be re-initiated.` )
114
+ throw Error ( deviceCodeExpiredMsg )
88
115
} else if ( err . code === 'TimeoutException' ) {
89
116
retryInterval *= 2
90
117
} else {
91
118
throw err
92
119
}
93
120
}
94
121
if ( Date . now ( ) + retryInterval > deviceCodeExpiration ) {
95
- throw Error ( `Device code has expired while polling for SSO token, login flow must be re-initiated.` )
122
+ throw Error ( deviceCodeExpiredMsg )
96
123
}
97
- // Delay each attempt by the interval
124
+ // Wait `retryInterval` milliseconds before next poll attempt.
98
125
await new Promise ( resolve => setTimeout ( resolve , retryInterval ) )
99
126
}
100
127
}
101
128
129
+ /**
130
+ * SSO step 2: start device authorization.
131
+ */
102
132
public async authorizeClient ( registration : SsoClientRegistration ) : Promise < StartDeviceAuthorizationResponse > {
103
133
const authorizationParams = {
104
134
clientId : registration . clientId ,
@@ -123,6 +153,9 @@ export class SsoAccessTokenProvider {
123
153
}
124
154
}
125
155
156
+ /**
157
+ * SSO step 1: get a client id.
158
+ */
126
159
public async registerClient ( ) : Promise < SsoClientRegistration > {
127
160
const currentRegistration = this . cache . loadClientRegistration ( this . ssoRegion )
128
161
if ( currentRegistration ) {
@@ -135,9 +168,7 @@ export class SsoAccessTokenProvider {
135
168
clientName : CLIENT_NAME ,
136
169
}
137
170
const registerResponse = await this . ssoOidcClient . registerClient ( registerParams ) . promise ( )
138
- const formattedExpiry = new Date (
139
- registerResponse . clientSecretExpiresAt ! * MILLISECONDS_PER_SECOND
140
- ) . toISOString ( )
171
+ const formattedExpiry = new Date ( registerResponse . clientSecretExpiresAt ! * MS_PER_SECOND ) . toISOString ( )
141
172
142
173
const registration : SsoClientRegistration = {
143
174
clientId : registerResponse . clientId ! ,
@@ -155,6 +186,6 @@ export class SsoAccessTokenProvider {
155
186
* @param seconds Number of seconds to add
156
187
*/
157
188
private currentTimePlusSecondsInMs ( seconds : number ) {
158
- return seconds * MILLISECONDS_PER_SECOND + Date . now ( )
189
+ return seconds * MS_PER_SECOND + Date . now ( )
159
190
}
160
191
}
0 commit comments