Skip to content

Commit e80aef1

Browse files
authored
SSO: documentation, minor changes #1570
- Add some comments that related the SSO specification to the Toolkit code.
1 parent cf4d477 commit e80aef1

File tree

2 files changed

+61
-27
lines changed

2 files changed

+61
-27
lines changed

src/credentials/sso/ssoAccessTokenProvider.ts

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,38 @@ import { StartDeviceAuthorizationResponse } from 'aws-sdk/clients/ssooidc'
1212

1313
const CLIENT_REGISTRATION_TYPE = 'public'
1414
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.
1616
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
1918
const BACKOFF_DELAY_MS = 5000
2019

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+
*/
2147
export class SsoAccessTokenProvider {
2248
public constructor(
2349
private ssoRegion: string,
@@ -31,8 +57,11 @@ export class SsoAccessTokenProvider {
3157
if (accessToken) {
3258
return accessToken
3359
}
60+
// SSO step 1
3461
const registration = await this.registerClient()
62+
// SSO step 2
3563
const authorization = await this.authorizeClient(registration)
64+
// SSO step 3
3665
const token = await this.pollForToken(registration, authorization)
3766
this.cache.saveAccessToken(this.ssoUrl, token)
3867
return token
@@ -42,30 +71,28 @@ export class SsoAccessTokenProvider {
4271
this.cache.invalidateAccessToken(this.ssoUrl)
4372
}
4473

74+
/**
75+
* SSO step 3: poll for the access token.
76+
*/
4577
private async pollForToken(
4678
registration: SsoClientRegistration,
47-
authorization: StartDeviceAuthorizationResponse
79+
authz: StartDeviceAuthorizationResponse
4880
): 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
6390

6491
const createTokenParams = {
6592
clientId: registration.clientId,
6693
clientSecret: registration.clientSecret,
6794
grantType: GRANT_TYPE,
68-
deviceCode: authorization.deviceCode!,
95+
deviceCode: authz.deviceCode!,
6996
}
7097

7198
while (true) {
@@ -82,23 +109,26 @@ export class SsoAccessTokenProvider {
82109
if (err.code === 'SlowDownException') {
83110
retryInterval += BACKOFF_DELAY_MS
84111
} else if (err.code === 'AuthorizationPendingException') {
85-
// do nothing, wait the interval and try again
112+
// Do nothing, try again after the interval.
86113
} 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)
88115
} else if (err.code === 'TimeoutException') {
89116
retryInterval *= 2
90117
} else {
91118
throw err
92119
}
93120
}
94121
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)
96123
}
97-
// Delay each attempt by the interval
124+
// Wait `retryInterval` milliseconds before next poll attempt.
98125
await new Promise(resolve => setTimeout(resolve, retryInterval))
99126
}
100127
}
101128

129+
/**
130+
* SSO step 2: start device authorization.
131+
*/
102132
public async authorizeClient(registration: SsoClientRegistration): Promise<StartDeviceAuthorizationResponse> {
103133
const authorizationParams = {
104134
clientId: registration.clientId,
@@ -123,6 +153,9 @@ export class SsoAccessTokenProvider {
123153
}
124154
}
125155

156+
/**
157+
* SSO step 1: get a client id.
158+
*/
126159
public async registerClient(): Promise<SsoClientRegistration> {
127160
const currentRegistration = this.cache.loadClientRegistration(this.ssoRegion)
128161
if (currentRegistration) {
@@ -135,9 +168,7 @@ export class SsoAccessTokenProvider {
135168
clientName: CLIENT_NAME,
136169
}
137170
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()
141172

142173
const registration: SsoClientRegistration = {
143174
clientId: registerResponse.clientId!,
@@ -155,6 +186,6 @@ export class SsoAccessTokenProvider {
155186
* @param seconds Number of seconds to add
156187
*/
157188
private currentTimePlusSecondsInMs(seconds: number) {
158-
return seconds * MILLISECONDS_PER_SECOND + Date.now()
189+
return seconds * MS_PER_SECOND + Date.now()
159190
}
160191
}

src/shared/telemetry/README

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ clienttelemetry.d.ts is autogenerated from service-2.json using the AWS JS SDK.
66
`postinstall` npm step.
77

88
\build-scripts\generateServiceClient.ts automates the process of generating clienttelemetry.d.ts
9+
10+
Client ID is generated and stored/retrieved in `createDefaultPublisher()`:
11+
https://github.com/aws/aws-toolkit-vscode/blob/d3cd90c114b5be6b2cfabad4bfa5670f0b7adf60/src/shared/telemetry/defaultTelemetryService.ts#L163-L168

0 commit comments

Comments
 (0)