Files
openid-client/tap/end2end-device-code.ts

146 lines
3.9 KiB
TypeScript

import type QUnit from 'qunit'
import { setup } from './helper.js'
import * as client from '../src/index.js'
import * as jose from 'jose'
export default (QUnit: QUnit) => {
const { module, test } = QUnit
module('end2end-device-code.ts')
const alg = 'ES256'
for (const feat of [undefined, 'dpop', 'abort', 'encryption']) {
let dpop = false
let abort = false
let encryption = false
let title: string
switch (feat) {
case 'dpop':
title = 'end-to-end device flow w/ dpop'
dpop = true
break
case 'abort':
title = 'end-to-end device flow w/ abort'
abort = true
break
case 'encryption':
title = 'end-to-end device flow w/ encryption'
encryption = true
break
default:
title = 'end-to-end device flow'
break
}
test(title, async (t) => {
const kp = (await jose.generateKeyPair('ES256', {
extractable: true,
})) as client.CryptoKeyPair
const { metadata, issuerIdentifier, clientDecryptionKey } = await setup(
kp,
'client_secret_post',
false,
false,
false,
['refresh_token', 'urn:ietf:params:oauth:grant-type:device_code'],
encryption,
)
const config = await client.discovery(
issuerIdentifier,
metadata.client_id,
metadata,
undefined,
{
execute: [
client.allowInsecureRequests,
client.enableNonRepudiationChecks,
],
},
)
if (encryption) {
client.enableDecryptingResponses(config, undefined, clientDecryptionKey)
}
const DPoP = dpop
? client.getDPoPHandle(config, await client.randomDPoPKeyPair(alg))
: undefined
const resource = 'urn:example:resource:jwt'
const params = new URLSearchParams()
params.set('resource', resource)
params.set('scope', 'openid api:write')
const deviceAuthorizationResponse =
await client.initiateDeviceAuthorization(config, params)
if (!abort) {
fetch('http://localhost:3000/drive', {
method: 'POST',
body: new URLSearchParams({
goto: deviceAuthorizationResponse.verification_uri_complete!,
}),
})
}
{
let signal: AbortSignal | undefined
if (abort) {
const controller = new AbortController()
signal = controller.signal
setTimeout(() => controller.abort(), 250)
}
let result: client.TokenEndpointResponse
const polling = client.pollDeviceAuthorizationGrant(
config,
deviceAuthorizationResponse,
undefined,
{ DPoP, signal },
)
if (abort) {
const err = await polling.catch((err) => err)
t.true(err instanceof Error)
t.propContains(err, { code: 'OAUTH_ABORT' })
t.equal(err.cause.name, 'AbortError')
return
} else {
result = await polling
}
const { access_token, token_type } = result
t.ok(access_token)
t.equal(token_type, dpop ? 'dpop' : 'bearer')
await client
.fetchProtectedResource(
config,
access_token,
new URL(config.serverMetadata().userinfo_endpoint!),
'GET',
undefined,
undefined,
{ DPoP },
)
.then(() => {
t.ok(0)
})
.catch((err) => {
if (err instanceof client.WWWAuthenticateChallengeError) {
const [{ parameters }] = err.cause
// TODO: check why the server responds with scheme bearer in dpop case
// t.equal(scheme, dpop ? 'dpop' : 'bearer')
t.equal(parameters.error, 'invalid_token')
} else {
t.ok(0)
}
})
}
t.ok(1)
})
}
}