Skip to content

Commit a2a90fd

Browse files
authored
fix(auth): more logging in OIDC client #3265
* Add extra logging to the OIDC client * Log exceptions * Add telemetry
1 parent dc49645 commit a2a90fd

File tree

5 files changed

+107
-41
lines changed

5 files changed

+107
-41
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3468,7 +3468,7 @@
34683468
"report": "nyc report --reporter=html --reporter=json"
34693469
},
34703470
"devDependencies": {
3471-
"@aws-toolkits/telemetry": "^1.0.100",
3471+
"@aws-toolkits/telemetry": "^1.0.120",
34723472
"@cspotcode/source-map-support": "^0.8.1",
34733473
"@sinonjs/fake-timers": "^8.1.0",
34743474
"@types/adm-zip": "^0.4.34",

src/credentials/sso/clients.ts

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
SSOServiceException,
1717
} from '@aws-sdk/client-sso'
1818
import {
19+
AuthorizationPendingException,
1920
CreateTokenRequest,
2021
RegisterClientRequest,
2122
SSOOIDC,
@@ -28,6 +29,9 @@ import { getLogger } from '../../shared/logger'
2829
import { SsoAccessTokenProvider } from './ssoAccessTokenProvider'
2930
import { isClientFault } from '../../shared/errors'
3031
import { DevSettings } from '../../shared/settings'
32+
import { Client } from '@aws-sdk/smithy-client'
33+
import { HttpHandlerOptions } from '@aws-sdk/types'
34+
import { HttpRequest, HttpResponse } from '@aws-sdk/protocol-http'
3135

3236
export class OidcClient {
3337
public constructor(private readonly client: SSOOIDC, private readonly clock: { Date: typeof Date }) {}
@@ -66,13 +70,13 @@ export class OidcClient {
6670
}
6771

6872
public static create(region: string) {
69-
return new this(
70-
new SSOOIDC({
71-
region,
72-
endpoint: DevSettings.instance.get('endpoints', {})['ssooidc'],
73-
}),
74-
globals.clock
75-
)
73+
const client = new SSOOIDC({
74+
region,
75+
endpoint: DevSettings.instance.get('endpoints', {})['ssooidc'],
76+
})
77+
78+
addLoggingMiddleware(client)
79+
return new this(client, globals.clock)
7680
}
7781
}
7882

@@ -174,3 +178,52 @@ export class SsoClient {
174178
)
175179
}
176180
}
181+
182+
function omitIfPresent<T extends Record<string, unknown>>(obj: T, ...keys: string[]): T {
183+
const objCopy = { ...obj }
184+
for (const key of keys) {
185+
if (key in objCopy) {
186+
;(objCopy as any)[key] = '[omitted]'
187+
}
188+
}
189+
return objCopy
190+
}
191+
192+
function addLoggingMiddleware(client: Client<HttpHandlerOptions, any, any, any>) {
193+
client.middlewareStack.add(
194+
(next, context) => args => {
195+
if (HttpRequest.isInstance(args.request)) {
196+
const { hostname, path } = args.request
197+
const input = omitIfPresent(args.input, 'clientSecret', 'accessToken', 'refreshToken')
198+
getLogger().debug('API request (%s %s): %O', hostname, path, input)
199+
}
200+
return next(args)
201+
},
202+
{ step: 'finalizeRequest' }
203+
)
204+
205+
client.middlewareStack.add(
206+
(next, context) => async args => {
207+
if (!HttpRequest.isInstance(args.request)) {
208+
return next(args)
209+
}
210+
211+
const { hostname, path } = args.request
212+
const result = await next(args).catch(e => {
213+
if (e instanceof Error && !(e instanceof AuthorizationPendingException)) {
214+
const err = { ...e }
215+
delete err['stack']
216+
getLogger().error('API response (%s %s): %O', hostname, path, err)
217+
}
218+
throw e
219+
})
220+
if (HttpResponse.isInstance(result.response)) {
221+
const output = omitIfPresent(result.output, 'clientSecret', 'accessToken', 'refreshToken')
222+
getLogger().debug('API response (%s %s): %O', hostname, path, output)
223+
}
224+
225+
return result
226+
},
227+
{ step: 'deserialize' }
228+
)
229+
}

src/credentials/sso/ssoAccessTokenProvider.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ const localize = nls.loadMessageBundle()
99
import globals from '../../shared/extensionGlobals'
1010
import * as vscode from 'vscode'
1111
import { AuthorizationPendingException, SlowDownException, SSOOIDCServiceException } from '@aws-sdk/client-sso-oidc'
12-
import { openSsoPortalLink, SsoToken, ClientRegistration, isExpired, SsoProfile } from './model'
12+
import { openSsoPortalLink, SsoToken, ClientRegistration, isExpired, SsoProfile, builderIdStartUrl } from './model'
1313
import { getCache } from './cache'
1414
import { hasProps, hasStringProps, RequiredProps, selectFrom } from '../../shared/utilities/tsUtils'
1515
import { CancellationError, sleep } from '../../shared/utilities/timeoutUtils'
1616
import { OidcClient } from './clients'
1717
import { loadOr } from '../../shared/utilities/cacheUtils'
18-
import { isClientFault, ToolkitError } from '../../shared/errors'
18+
import { getTelemetryReason, getTelemetryResult, isClientFault, ToolkitError } from '../../shared/errors'
19+
import { getLogger } from '../../shared/logger'
20+
import { telemetry } from '../../shared/telemetry/telemetry'
1921

2022
const clientRegistrationType = 'public'
2123
const deviceGrantType = 'urn:ietf:params:oauth:grant-type:device_code'
@@ -81,11 +83,7 @@ export class SsoAccessTokenProvider {
8183
if (data.registration && !isExpired(data.registration) && hasProps(data.token, 'refreshToken')) {
8284
const refreshed = await this.refreshToken(data.token, data.registration)
8385

84-
if (refreshed) {
85-
await this.cache.token.save(this.tokenCacheKey, refreshed)
86-
}
87-
88-
return refreshed?.token
86+
return refreshed.token
8987
} else {
9088
await this.invalidate()
9189
}
@@ -95,6 +93,7 @@ export class SsoAccessTokenProvider {
9593
const access = await this.runFlow()
9694
const identity = (await identityProvider?.(access.token)) ?? this.tokenCacheKey
9795
await this.cache.token.save(identity, access)
96+
await setSessionCreationDate(this.tokenCacheKey, new Date())
9897

9998
return { ...access.token, identity }
10099
}
@@ -118,9 +117,19 @@ export class SsoAccessTokenProvider {
118117
try {
119118
const clientInfo = selectFrom(registration, 'clientId', 'clientSecret')
120119
const response = await this.oidc.createToken({ ...clientInfo, ...token, grantType: refreshGrantType })
120+
const refreshed = this.formatToken(response, registration)
121+
await this.cache.token.save(this.tokenCacheKey, refreshed)
121122

122-
return this.formatToken(response, registration)
123+
return refreshed
123124
} catch (err) {
125+
telemetry.aws_refreshCredentials.emit({
126+
result: getTelemetryResult(err),
127+
reason: getTelemetryReason(err),
128+
sessionDuration: getSessionDuration(this.tokenCacheKey),
129+
credentialType: 'bearerToken',
130+
credentialSourceId: this.profile.startUrl === builderIdStartUrl ? 'awsId' : 'iamIdentityCenter',
131+
})
132+
124133
if (err instanceof SSOOIDCServiceException && isClientFault(err)) {
125134
await this.cache.token.clear(this.tokenCacheKey)
126135
}
@@ -228,3 +237,25 @@ async function pollForTokenWithProgress<T>(
228237
])
229238
)
230239
}
240+
241+
const sessionCreationDateKey = '#sessionCreationDates'
242+
async function setSessionCreationDate(id: string, date: Date, memento = globals.context.globalState) {
243+
try {
244+
await memento.update(sessionCreationDateKey, {
245+
...memento.get(sessionCreationDateKey),
246+
[id]: date.getTime(),
247+
})
248+
} catch (err) {
249+
getLogger().verbose('auth: failed to set session creation date: %s', err)
250+
}
251+
}
252+
253+
function getSessionCreationDate(id: string, memento = globals.context.globalState): number | undefined {
254+
return memento.get(sessionCreationDateKey, {} as Record<string, number>)[id]
255+
}
256+
257+
function getSessionDuration(id: string, memento = globals.context.globalState) {
258+
const creationDate = getSessionCreationDate(id, memento)
259+
260+
return creationDate !== undefined ? Date.now() - creationDate : undefined
261+
}

src/shared/telemetry/vscodeTelemetry.json

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,24 +59,6 @@
5959
"description": "Called after attempting to install a local copy of a missing CLI",
6060
"metadata": [{ "type": "result" }, { "type": "cli" }]
6161
},
62-
{
63-
"name": "aws_refreshCredentials",
64-
"description": "Emitted when credentials are automatically refreshed by the AWS SDK",
65-
"passive": true,
66-
"metadata": [
67-
{
68-
"type": "result"
69-
},
70-
{
71-
"type": "credentialType",
72-
"required": false
73-
},
74-
{
75-
"type": "credentialSourceId",
76-
"required": false
77-
}
78-
]
79-
},
8062
{
8163
"name": "ssm_createDocument",
8264
"description": "An SSM Document is created locally",

0 commit comments

Comments
 (0)