diff --git a/manifest.json b/manifest.json index f509733..55150d4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "API console extension", - "version": "0.1.0", + "version": "0.1.3", "description": "API Console extension to proxy HTTP requests to the documented API.", "author": "Pawel Psztyc ", "content_scripts": [ diff --git a/package-lock.json b/package-lock.json index ebe1699..e450e58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@advanced-rest-client/api-console-extension", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@advanced-rest-client/api-console-extension", - "version": "0.1.2", + "version": "0.1.3", "license": "CC-BY-2.0", "devDependencies": { "@commitlint/cli": "^17.0.0", diff --git a/package.json b/package.json index a4586e6..311da94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@advanced-rest-client/api-console-extension", - "version": "0.1.2", + "version": "0.1.3", "description": "API Console extension to proxy HTTP requests to the documented API.", "type": "module", "main": "index.js", diff --git a/src/ApiConsoleAppProxy.js b/src/ApiConsoleAppProxy.js index 429399f..f585ccf 100644 --- a/src/ApiConsoleAppProxy.js +++ b/src/ApiConsoleAppProxy.js @@ -234,6 +234,7 @@ export class ApiConsoleAppProxy extends EventTarget { detail: { message: "No response has been recorded.", code: "no_response", + error: true, }, }) ); @@ -241,12 +242,9 @@ export class ApiConsoleAppProxy extends EventTarget { } const typedError = /** @type IApiConsoleProxyError */ (data); if (typedError.error) { - const message = - typeof typedError.message === "string" - ? typedError.message - : "No response has been recorded."; - const code = - typeof typedError.code === "string" ? typedError.code : "unknown_error"; + const message = typeof typedError.message === "string" ? typedError.message : "No response has been recorded."; + const code = typeof typedError.code === "string" ? typedError.code : "unknown_error"; + const state = typeof typedError.state === "string" ? typedError.state : undefined; this.eventTarget.dispatchEvent( new CustomEvent("oauth2-error", { bubbles: true, @@ -254,31 +252,22 @@ export class ApiConsoleAppProxy extends EventTarget { detail: { message, code, + state, + error: true, }, }) ); return; } const typedToken = /** @type ITokenInfo */ (data); - const state = - typeof typedToken.state === "string" ? typedToken.state : undefined; - const accessToken = - typedToken.accessToken && typeof typedToken.accessToken === "string" - ? typedToken.accessToken - : undefined; - const tokenType = - typedToken.tokenType && typeof typedToken.tokenType === "string" - ? typedToken.tokenType - : undefined; - const expiresIn = - typedToken.expiresIn && - (typeof typedToken.expiresIn === "number" || - typeof typedToken.expiresIn === "string") - ? Number(typedToken.expiresIn) - : undefined; - const scope = Array.isArray(typedToken.scope) - ? typedToken.scope - : undefined; + const state = typeof typedToken.state === "string" ? typedToken.state : undefined; + const accessToken = typedToken.accessToken && typeof typedToken.accessToken === "string" ? typedToken.accessToken : undefined; + const refreshToken = typedToken.refreshToken && typeof typedToken.refreshToken === "string" ? typedToken.refreshToken : undefined; + const tokenType = typedToken.tokenType && typeof typedToken.tokenType === "string" ? typedToken.tokenType : undefined; + const expiresIn = typedToken.expiresIn && (typeof typedToken.expiresIn === "number" || typeof typedToken.expiresIn === "string") ? Number(typedToken.expiresIn): undefined; + const expiresAt = typedToken.expiresAt && (typeof typedToken.expiresAt === "number" || typeof typedToken.expiresAt === "string") ? Number(typedToken.expiresAt): undefined; + const expiresAssumed = typeof typedToken.expiresAssumed === "boolean" ? typedToken.expiresAssumed : undefined; + const scope = Array.isArray(typedToken.scope) ? typedToken.scope : undefined; this.eventTarget.dispatchEvent( new CustomEvent("oauth2-token-response", { bubbles: true, @@ -289,6 +278,9 @@ export class ApiConsoleAppProxy extends EventTarget { tokenType, expiresIn, scope, + refreshToken, + expiresAt, + expiresAssumed, }, }) ); diff --git a/src/proxy/OAuth2Proxy.js b/src/proxy/OAuth2Proxy.js index 2205507..8527ac5 100644 --- a/src/proxy/OAuth2Proxy.js +++ b/src/proxy/OAuth2Proxy.js @@ -250,6 +250,7 @@ export class OAuth2Proxy { id: id, tab: tab }; + this._addTabHandlers(); } catch (e) { throw new AuthorizationError( e.message, @@ -343,7 +344,7 @@ export class OAuth2Proxy { /** @type string */ let raw; try { - const raw = this._authDataFromUrl(url); + raw = this._authDataFromUrl(url); if (!raw) { throw new Error(''); } @@ -437,6 +438,7 @@ export class OAuth2Proxy { let tokenInfo; try { tokenInfo = await this.exchangeCode(code); + tokenInfo.state = state; } catch (e) { this._handleTokenCodeError(/** @type Error */(e)); return; diff --git a/src/proxy/OAuthUtils.js b/src/proxy/OAuthUtils.js index 7189d2b..6126ab5 100644 --- a/src/proxy/OAuthUtils.js +++ b/src/proxy/OAuthUtils.js @@ -59,7 +59,7 @@ export function sanityCheck(settings) { */ export function randomString() { const array = new Uint32Array(28); - window.crypto.getRandomValues(array); + globalThis.crypto.getRandomValues(array); return Array.from(array, (dec) => `0${dec.toString(16)}`.substr(-2)).join(""); } @@ -93,7 +93,7 @@ export function camel(name) { export async function sha256(value) { const encoder = new TextEncoder(); const data = encoder.encode(value); - return window.crypto.subtle.digest("SHA-256", data); + return globalThis.crypto.subtle.digest("SHA-256", data); } /** @@ -126,7 +126,7 @@ export async function generateCodeChallenge(verifier) { export function nonceGenerator(size = 20) { const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let array = new Uint8Array(size); - window.crypto.getRandomValues(array); + globalThis.crypto.getRandomValues(array); array = array.map(x => validChars.charCodeAt(x % validChars.length)); return String.fromCharCode.apply(null, array); } diff --git a/src/service.js b/src/service.js index 2742967..5fc903a 100644 --- a/src/service.js +++ b/src/service.js @@ -51,8 +51,9 @@ class ApiConsoleService { } catch (e) { this.sendResponse({ 'message': e.message || 'The request is invalid.', - 'code': 'invalid_request', - 'error': true + 'code': e.code || 'invalid_request', + 'error': true, + 'state': e.state, }); } } diff --git a/src/types.d.ts b/src/types.d.ts index 2e7245a..84251b8 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -108,6 +108,7 @@ export interface IApiConsoleProxyError { error: true; code?: string; message: string; + state?: string; } export interface IApiConsoleHttpResponseStats { diff --git a/test/authorization/ServerMock.js b/test/authorization/ServerMock.js new file mode 100644 index 0000000..56ee2d2 --- /dev/null +++ b/test/authorization/ServerMock.js @@ -0,0 +1,451 @@ +import crypto from 'crypto'; + +/** + * Generates a random string of characters. + * + * @returns {string} A random string. + */ +function randomString() { + return crypto.randomBytes(24).toString('base64').slice(0, 24); +} + +async function readBody(ctx) { + const { req } = ctx; + return new Promise((resolve) => { + let data = ''; + req.on('data', (chunk) => { + data += chunk; + }); + + req.on('end', () => { + resolve(data); + }); + }); +} + +const acceptableClientIds = ['auth-code-cid', 'custom-scopes', 'custom-data']; +const acceptableSecrets = ['auth-code-cs', 'cc-secret']; + +const responseToken = 'token1234'; +const refreshToken = 'refresh1234'; +const formContentType = 'application/x-www-form-urlencoded'; +const customScopes = 'c1 c2'; + +const codes = {}; +const codeChallenges = {}; + +export const CodeServerMock = { + authRequest(request) { + const { url } = request; + const query = url.replace('/auth-code?', ''); + const params = new URLSearchParams(query); + const redirect = params.get('redirect_uri'); + const state = params.get('state'); + const scope = params.get('scope'); + const clientId = params.get('client_id'); + let code; + if (clientId === 'invalid-code') { + code = 'invalid'; + } else { + code = randomString(); + codes[code] = clientId; + } + + const codeChallenge = params.get('code_challenge'); + let codeChallengeMethod = params.get('code_challenge_method'); + const failPkce = params.get('failPkce'); + if (codeChallenge) { + if (!codeChallengeMethod) { + codeChallengeMethod = 'plain'; + } + codeChallenges[code] = { + codeChallenge, + codeChallengeMethod, + }; + if (failPkce === 'true') { + codeChallenges[code].codeChallenge += 'a'; + } + } + + const newUrl = new URL(redirect); + newUrl.searchParams.set('state', state); + newUrl.searchParams.set('code', code); + if (scope) { + newUrl.searchParams.set('scope', scope); + } + let body = ''; + body += '

Auth server

'; + body += ``; + body += ''; + return { + body, + type: 'text/html', + }; + }, + + authRequestCustom(request) { + const { url } = request; + const query = url.replace('/auth-code?', ''); + + const params = new URLSearchParams(query); + const state = params.get('state'); + const custom = params.get('customQuery'); + const redirect = params.get('redirect_uri'); + + const newUrl = new URL(redirect); + newUrl.searchParams.set('state', state); + newUrl.searchParams.set('code', custom); + + let body = ''; + body += '

Auth server

'; + body += ``; + body += ''; + return { + body, + type: 'text/html', + }; + }, + + async authRequestImplicit(ctx) { + const { url } = ctx.request; + const query = url.replace('/oauth2/auth-implicit?', ''); + const params = new URLSearchParams(query); + const redirect = params.get('redirect_uri'); + const state = params.get('state'); + const scope = params.get('scope'); + const cid = params.get('client_id'); + + const newUrl = new URL(redirect); + newUrl.searchParams.set('state', state); + newUrl.searchParams.set('token_type', 'Bearer'); + + if (!acceptableClientIds.includes(cid)) { + newUrl.searchParams.set('error', 'invalid_client'); + } else { + newUrl.searchParams.set('access_token', responseToken); + newUrl.searchParams.set('refresh_token', refreshToken); + newUrl.searchParams.set('expires_in', '3600'); + if (cid === 'custom-scopes') { + newUrl.searchParams.set('scope', customScopes); + } else if (scope) { + newUrl.searchParams.set('scope', scope); + } + } + const final = `${redirect}#${newUrl.searchParams.toString()}`; + ctx.status = 301; + ctx.redirect(final); + }, + + async authRequestImplicitCustom(ctx) { + const { url } = ctx.request; + const query = url.replace('/oauth2/auth-implicit-custom?', ''); + const params = new URLSearchParams(query); + const redirect = params.get('redirect_uri'); + const state = params.get('state'); + const custom = params.get('customQuery'); + + const newUrl = new URL(redirect); + newUrl.searchParams.set('state', state); + + if (custom !== 'customQueryValue') { + newUrl.searchParams.set('error', 'invalid_client'); + } else { + newUrl.searchParams.set('access_token', responseToken); + newUrl.searchParams.set('refresh_token', refreshToken); + newUrl.searchParams.set('expires_in', '3600'); + } + const final = `${redirect}#${newUrl.searchParams.toString()}`; + ctx.status = 301; + ctx.redirect(final); + }, + + async authRequestImplicitStateError(ctx) { + const { url } = ctx.request; + const query = url.replace('/oauth2/auth-implicit-invalid-state?', ''); + const params = new URLSearchParams(query); + const redirect = params.get('redirect_uri'); + + const newUrl = new URL(redirect); + newUrl.searchParams.set('state', 'OtHeR'); + newUrl.searchParams.set('access_token', responseToken); + newUrl.searchParams.set('refresh_token', refreshToken); + newUrl.searchParams.set('expires_in', '3600'); + const final = `${redirect}#${newUrl.searchParams.toString()}`; + ctx.status = 301; + ctx.redirect(final); + }, + + async tokenRequest(ctx) { + const body = await readBody(ctx); + const params = new URLSearchParams(body); + const result = new URLSearchParams(); + const cid = params.get('client_id'); + const redirectUri = params.get('redirect_uri'); + const code = params.get('code'); + const secret = params.get('client_secret'); + const verifier = params.get('code_verifier'); + const verifierSettings = codeChallenges[code]; + const challenge = verifier && verifierSettings ? crypto.createHash('sha256').update(verifier).digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : undefined; + + if (params.get('grant_type') !== 'authorization_code') { + result.set('error', 'invalid_grant'); + } else if (!acceptableClientIds.includes(cid)) { + result.set('error', 'invalid_client'); + } else if (!redirectUri.includes('popup.html')) { + result.set('error', 'invalid_request'); + result.set('error_description', 'invalid redirect'); + } else if (!(code in codes)) { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid code'); + } else if (!acceptableSecrets.includes(secret)) { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid secret'); + } else if (verifier && !verifierSettings) { + result.set('error', 'invalid_request'); + result.set('error_description', 'code_challenge not found'); + } else if (verifier && verifierSettings.codeChallenge !== challenge) { + result.set('error', 'invalid_request'); + result.set('error_description', `invalid code_verifier`); + } else { + result.set('token_type', 'Bearer'); + result.set('access_token', responseToken); + result.set('refresh_token', refreshToken); + result.set('expires_in', '3600'); + if (cid === 'custom-scopes') { + result.set('scope', customScopes); + } + } + return { + body: result.toString(), + type: formContentType, + }; + }, + + async tokenRequestCustom(ctx) { + const body = await readBody(ctx); + + const params = new URLSearchParams(body); + const result = new URLSearchParams(); + + const code = params.get('code'); + const customBody = params.get('customBody'); + // this comes from `authRequestCustom()` where the code is set to the value of the `customQuery` parameter + if (code !== 'customQueryValue') { + result.set('error', 'invalid_request'); + result.set('error_description', 'auth.parameters.customQuery is not set'); + } else if (customBody !== 'customBodyValue') { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.body.customBody is not set'); + } else if (!ctx.request.url.includes('customParameter=customParameterValue')) { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.parameters.customParameter is not set'); + } else if (ctx.request.header.customheader !== 'customHeaderValue') { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.headers.customHeader is not set'); + } else { + result.set('access_token', responseToken); + result.set('refresh_token', refreshToken); + result.set('token_type', 'Bearer'); + result.set('expires_in', '3600'); + } + return { + body: result.toString(), + type: formContentType, + }; + }, + + async tokenPassword(ctx) { + const body = await readBody(ctx); + + const params = new URLSearchParams(body); + const result = new URLSearchParams(); + + const username = params.get('username'); + const password = params.get('password'); + const cid = params.get('client_id'); + const scope = params.get('scope'); + const customBody = params.get('customBody'); + + if (!acceptableClientIds.includes(cid)) { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid client id'); + } else if (scope && scope !== 'a b') { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid scope'); + } else if (username !== 'test-uname') { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid username'); + } else if (password !== 'test-passwd') { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid password'); + } else if (cid === 'custom-data' && customBody !== 'customBodyValue') { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.body.customBody is not set'); + } else if (cid === 'custom-data' && !ctx.request.url.includes('customParameter=customParameterValue')) { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.parameters.customParameter is not set'); + } else if (cid === 'custom-data' && ctx.request.header.customheader !== 'customHeaderValue') { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.headers.customHeader is not set'); + } else { + result.set('access_token', responseToken); + result.set('refresh_token', refreshToken); + result.set('expires_in', '3600'); + result.set('token_type', 'Bearer'); + if (!scope) { + result.set('scope', 'custom'); + } else { + result.set('scope', scope); + } + } + return { + body: result.toString(), + type: formContentType, + }; + }, + + async tokenClientCredentials(ctx) { + const body = await readBody(ctx); + + const params = new URLSearchParams(body); + const result = new URLSearchParams(); + + const cid = params.get('client_id'); + const scope = params.get('scope'); + const secret = params.get('client_secret'); + const customBody = params.get('customBody'); + + if (cid && !acceptableClientIds.includes(cid)) { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid client id'); + } else if (secret && !acceptableSecrets.includes(secret)) { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid secret'); + } else if (scope && scope !== 'a b') { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid scope'); + } else if (cid === 'custom-data' && customBody !== 'customBodyValue') { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.body.customBody is not set'); + } else if (cid === 'custom-data' && !ctx.request.url.includes('customParameter=customParameterValue')) { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.parameters.customParameter is not set'); + } else if (cid === 'custom-data' && ctx.request.header.customheader !== 'customHeaderValue') { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.headers.customHeader is not set'); + } else { + result.set('access_token', responseToken); + result.set('refresh_token', refreshToken); + result.set('expires_in', '3600'); + result.set('token_type', 'Bearer'); + if (!scope) { + result.set('scope', 'custom'); + } else { + result.set('scope', scope); + } + } + return { + body: result.toString(), + type: formContentType, + }; + }, + + async tokenClientCredentialsHeader(ctx) { + const body = await readBody(ctx); + + const params = new URLSearchParams(body); + const result = new URLSearchParams(); + + const qpCid = params.get('client_id'); + const qpSecret = params.get('client_secret'); + const scope = params.get('scope'); + + const { headers } = ctx.request; + const { authorization } = headers; + if (!authorization) { + result.set('error', 'invalid_request'); + result.set('error_description', 'authorization header not set'); + } else if (qpCid) { + result.set('error', 'invalid_request'); + result.set('error_description', 'client_id not allowed in query parameters'); + } else if (qpSecret) { + result.set('error', 'invalid_request'); + result.set('error_description', 'client_secret not allowed in query parameters'); + } else if (scope && scope !== 'a b') { + result.set('error', 'invalid_request'); + result.set('error_description', 'invalid scope'); + } else { + const hash = authorization.replace(/basic /i, ''); + // eslint-disable-next-line no-undef + const unHashed = Buffer.from(hash, 'base64').toString(); + const [cid, secret] = unHashed.split(':'); + if (!cid) { + result.set('error', 'invalid_request'); + result.set('error_description', 'client_id is not set'); + } else if (!secret) { + result.set('error', 'invalid_request'); + result.set('error_description', 'client_secret is not set'); + } else { + result.set('access_token', responseToken); + result.set('refresh_token', refreshToken); + result.set('expires_in', '3600'); + result.set('token_type', 'Bearer'); + result.set('scope', scope); + } + } + + return { + body: result.toString(), + type: formContentType, + }; + }, + + async tokenCustomGrant(ctx) { + const body = await readBody(ctx); + + const params = new URLSearchParams(body); + const result = new URLSearchParams(); + + const cid = params.get('client_id'); + const grantType = params.get('grant_type'); + const scope = params.get('scope'); + const secret = params.get('client_secret'); + const customBody = params.get('customBody'); + + if (grantType !== 'custom-grant') { + result.set('error', 'invalid_grant'); + } else if (cid && !acceptableClientIds.includes(cid)) { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid client id'); + } else if (secret && !acceptableSecrets.includes(secret)) { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid secret'); + } else if (scope && scope !== 'a b') { + result.set('error', 'invalid_client'); + result.set('error_description', 'invalid scope'); + } else if (cid === 'custom-data' && customBody !== 'customBodyValue') { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.body.customBody is not set'); + } else if (cid === 'custom-data' && !ctx.request.url.includes('customParameter=customParameterValue')) { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.parameters.customParameter is not set'); + } else if (cid === 'custom-data' && ctx.request.header.customheader !== 'customHeaderValue') { + result.set('error', 'invalid_request'); + result.set('error_description', 'token.headers.customHeader is not set'); + } else { + result.set('access_token', responseToken); + result.set('refresh_token', refreshToken); + result.set('expires_in', '3600'); + result.set('token_type', 'Bearer'); + if (!scope) { + result.set('scope', 'custom'); + } else { + result.set('scope', scope); + } + } + return { + body: result.toString(), + type: formContentType, + }; + }, +}; diff --git a/test/authorization/popup.html b/test/authorization/popup.html new file mode 100644 index 0000000..b8391f1 --- /dev/null +++ b/test/authorization/popup.html @@ -0,0 +1,29 @@ + + + + + Oauth2 callback window + + + +

Sending the authorization data to the application

+ + + + diff --git a/test/authorization/wrong-redirect.html b/test/authorization/wrong-redirect.html new file mode 100644 index 0000000..b8391f1 --- /dev/null +++ b/test/authorization/wrong-redirect.html @@ -0,0 +1,29 @@ + + + + + Oauth2 callback window + + + +

Sending the authorization data to the application

+ + + + diff --git a/test/http-proxy.test.js b/test/http-proxy.test.js index be316e9..f454286 100644 --- a/test/http-proxy.test.js +++ b/test/http-proxy.test.js @@ -149,122 +149,3 @@ test.describe('HTTP Proxy', () => { expect(body.data).toEqual('test file contents'); }); }); - -// const assert = require('chai').assert; -// const ApiConsoleOauthProxy = require('../js/background.js').ApiConsoleOauthProxy; - -// describe('api-console-extension', function() { -// describe('OAuth2 proxy', function() { -// describe('popup-url', function() { - -// var proxy; -// var settings; -// var popupUrl; - -// before(function() { -// settings = { -// authorizationUrl: 'https://authorizationUrl.com', -// clientId: 'test-123 test', -// redirectUrl: 'https://redirectUrl.com', -// scopes: ['scope-1', 'scope-2'] -// }; -// proxy = new ApiConsoleOauthProxy(settings); -// popupUrl = proxy._constructPopupUrl('token'); -// }); - -// // beforeEach(function() { -// // proxy = new ApiConsoleOauthProxy(settings); -// // }); - -// function getParam(name) { -// var _url = popupUrl.substr(popupUrl.indexOf('?') + 1); -// var parts = _url.split('&'); -// for (var i = 0, len = parts.length; i < len; i++) { -// let params = parts[i].split('='); -// if (params[0] === name) { -// return params[1]; -// } -// } -// } - -// it('Constructs OAuth URL', function() { -// assert.isString(popupUrl); -// }); - -// it('Sets authorization URL and response_type', function() { -// var base = settings.authorizationUrl + '?response_type=token'; -// var index = popupUrl.indexOf(base); -// assert.equal(index, 0); -// }); - -// it('Sets client_id', function() { -// var clientId = getParam('client_id'); -// assert.equal(clientId, 'test-123%20test'); -// }); - -// it('Sets redirect_uri', function() { -// var redirectUrl = getParam('redirect_uri'); -// assert.equal(redirectUrl, 'https%3A%2F%2FredirectUrl.com'); -// }); - -// it('Sets scope', function() { -// var scopes = getParam('scope'); -// assert.equal(scopes, 'scope-1%20scope-2'); -// }); - -// it('Sets state', function() { -// var state = getParam('state'); -// assert.isString(state); -// }); -// }); - -// describe('authDataFromUrl', function() { -// var authData; -// var token; -// var tokenType; -// var expiresIn; -// var state; - -// before(function() { -// var settings = { -// authorizationUrl: 'https://authorizationUrl.com', -// clientId: 'test-123 test', -// redirectUrl: 'https://redirectUrl.com', -// scopes: ['scope-1', 'scope-2'] -// }; -// token = 'ya29.GlwpBBGitx7n81P6Jdu1l43Y0M_j7WD0uVQRc3H1v6PyL0Ob6H6UrsWj'; -// token += '-rTMxXtX66_cdEbRJwHyArtR79GIGnIYfhcOBMt8qH96e9oGswGaGPkb1egRZ5UIf_qzFQ'; -// tokenType = 'Bearer'; -// state = '173mwy'; -// expiresIn = '3600'; -// var proxy = new ApiConsoleOauthProxy(settings); -// var url = 'http://localhost:8080/components/oauth-authorization/oauth-popup.html'; -// url += '#state=' + state; -// url += '&access_token=' + token; -// url += '&token_type=' + tokenType; -// url += '&expires_in=' + expiresIn; -// authData = proxy.authDataFromUrl(url); -// }); - -// it('Has accessToken', function() { -// assert.isString(authData.accessToken, 'accessToken is string'); -// assert.equal(authData.accessToken, token, 'Token value equals'); -// }); - -// it('Has accessToken', function() { -// assert.isString(authData.expiresIn, 'expiresIn is string'); -// assert.equal(authData.expiresIn, expiresIn, 'expiresIn equals ' + expiresIn); -// }); - -// it('Has state', function() { -// assert.isString(authData.state, 'state is string'); -// assert.equal(authData.state, state, 'state equals ' + state); -// }); - -// it('Has tokenType', function() { -// assert.isString(authData.tokenType, 'tokenType is string'); -// assert.equal(authData.tokenType, tokenType, 'tokenType equals ' + tokenType); -// }); -// }); -// }); -// }); diff --git a/test/models/ProxyRequest.js b/test/models/ProxyRequest.js index d8ed85d..80da71d 100644 --- a/test/models/ProxyRequest.js +++ b/test/models/ProxyRequest.js @@ -1,4 +1,5 @@ /** @typedef {import('../../src/types').ISafePayload} ISafePayload */ +/** @typedef {import('../../src/types').IOAuth2Authorization} IOAuth2Authorization */ export class ProxyRequest { /** @@ -73,6 +74,36 @@ export class ProxyRequest { return result; } + /** + * Dispatches the event for the proxy to handle OAuth 2 authorization. + * @param {IOAuth2Authorization} config + */ + async proxyOauth2(config) { + const result = await this.page.evaluate(([detail]) => { + const e = new CustomEvent('oauth2-token-requested', { + bubbles: true, + cancelable: true, + detail, + }); + document.body.dispatchEvent(e); + return new Promise((resolve) => { + const handlerSuccess = (e) => { + resolve(e.detail); + window.removeEventListener('oauth2-token-response', handlerSuccess); + window.removeEventListener('oauth2-error', handlerError); + }; + const handlerError = (e) => { + resolve(e.detail); + window.removeEventListener('oauth2-token-response', handlerSuccess); + window.removeEventListener('oauth2-error', handlerError); + }; + window.addEventListener('oauth2-token-response', handlerSuccess); + window.addEventListener('oauth2-error', handlerError); + }); + }, [config]); + return result; + } + /** * @param {string} url * @param {string=} method diff --git a/test/oauth2-client-credentials-proxy.test.js b/test/oauth2-client-credentials-proxy.test.js new file mode 100644 index 0000000..8b517b7 --- /dev/null +++ b/test/oauth2-client-credentials-proxy.test.js @@ -0,0 +1,166 @@ +import { test as base, chromium, expect } from '@playwright/test'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { ProxyRequest } from './models/ProxyRequest.js'; + +/** @typedef {import('../src/types').IOAuth2Authorization} IOAuth2Authorization */ + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const extensionPath = join(__dirname, '..'); + +const test = base.extend({ + context: async ({ browserName }, use) => { + const browserTypes = { chromium }; + const launchOptions = { + devtools: true, + headless: false, + args: [ + `--disable-extensions-except=${extensionPath}` + ], + viewport: { + width: 1920, + height: 1080 + }, + }; + const context = await browserTypes[browserName].launchPersistentContext('', launchOptions); + await use(context); + await context.close(); + } +}); + +test.describe('OAuth 2.0 Proxy', () => { + test.describe('client credentials grant', () => { + test.describe('Body delivery method', () => { + /** @type ProxyRequest */ + let proxy; + test.beforeEach(async ({ page }) => { + proxy = new ProxyRequest(page); + await proxy.navigate(); + }); + + const baseConfig = /** @type IOAuth2Authorization */ ({ + grantType: 'client_credentials', + clientId: 'auth-code-cid', + clientSecret: 'cc-secret', + scopes: ['a', 'b'], + accessTokenUri: 'http://localhost:8000/oauth2/client-credentials', + }); + + test('returns the token info', async () => { + const config = { + ...baseConfig, + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.accessToken).toEqual('token1234'); + expect(tokenInfo.tokenType).toEqual('Bearer'); + expect(tokenInfo.refreshToken).toEqual('refresh1234'); + expect(tokenInfo.expiresIn).toStrictEqual(3600); + expect(tokenInfo.scope).toEqual(baseConfig.scopes); + expect(tokenInfo).toHaveProperty('expiresAt'); + expect(tokenInfo.expiresAssumed).toBe(false); + }); + + test('returns error when client_id error', async () => { + const config = { + ...baseConfig, + clientId: 'invalid' + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('invalid client id'); + expect(tokenInfo.code).toEqual('invalid_client'); + expect(tokenInfo.error).toEqual(true); + }); + + test('handles when invalid secret', async () => { + const config = { + ...baseConfig, + clientSecret: 'invalid', + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('invalid secret'); + expect(tokenInfo.code).toEqual('invalid_client'); + expect(tokenInfo.error).toEqual(true); + }); + }); + + test.describe('Headers delivery method', () => { + /** @type ProxyRequest */ + let proxy; + test.beforeEach(async ({ page }) => { + proxy = new ProxyRequest(page); + await proxy.navigate(); + }); + + const baseConfig = /** @type IOAuth2Authorization */ ({ + grantType: 'client_credentials', + clientId: 'auth-code-cid', + clientSecret: 'cc-secret', + scopes: ['a', 'b'], + accessTokenUri: 'http://localhost:8000/oauth2/client-credentials-header', + deliveryMethod: 'header', + deliveryName: 'authorization', + }); + + test('returns the token info', async () => { + const config = { + ...baseConfig, + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.accessToken).toEqual('token1234'); + expect(tokenInfo.tokenType).toEqual('Bearer'); + expect(tokenInfo.refreshToken).toEqual('refresh1234'); + expect(tokenInfo.expiresIn).toStrictEqual(3600); + expect(tokenInfo.scope).toEqual(baseConfig.scopes); + expect(tokenInfo).toHaveProperty('expiresAt'); + expect(tokenInfo.expiresAssumed).toBe(false); + }); + }); + + test.describe('custom data', () => { + /** @type ProxyRequest */ + let proxy; + test.beforeEach(async ({ page }) => { + proxy = new ProxyRequest(page); + await proxy.navigate(); + }); + + const baseConfig = /** @type IOAuth2Authorization */ ({ + grantType: 'client_credentials', + clientId: 'auth-code-cid', + clientSecret: 'cc-secret', + scopes: ['a', 'b'], + accessTokenUri: 'http://localhost:8000/oauth2/client-credentials', + customData: { + auth: { + parameters: [{ + name: 'customQuery', + value: 'customQueryValue' + }] + }, + token: { + body: [{ + name: 'customBody', + value: 'customBodyValue' + }], + headers: [{ + name: 'customHeader', + value: 'customHeaderValue' + }], + parameters: [{ + name: 'customParameter', + value: 'customParameterValue' + }], + }, + } + }); + + test('applies all custom data', async () => { + const config = { + ...baseConfig, + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.accessToken).toEqual('token1234'); + }); + }); + }); +}); diff --git a/test/oauth2-code-proxy.test.js b/test/oauth2-code-proxy.test.js new file mode 100644 index 0000000..3075d26 --- /dev/null +++ b/test/oauth2-code-proxy.test.js @@ -0,0 +1,177 @@ +import { test as base, chromium, expect } from '@playwright/test'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { ProxyRequest } from './models/ProxyRequest.js'; + +/** @typedef {import('../src/types').IOAuth2Authorization} IOAuth2Authorization */ + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const extensionPath = join(__dirname, '..'); + +const test = base.extend({ + context: async ({ browserName }, use) => { + const browserTypes = { chromium }; + const launchOptions = { + devtools: true, + headless: false, + args: [ + `--disable-extensions-except=${extensionPath}` + ], + viewport: { + width: 1920, + height: 1080 + }, + }; + const context = await browserTypes[browserName].launchPersistentContext('', launchOptions); + await use(context); + await context.close(); + } +}); + +test.describe('OAuth 2.0 Proxy', () => { + test.describe('authorization_code grant', () => { + /** @type ProxyRequest */ + let proxy; + test.beforeEach(async ({ page }) => { + proxy = new ProxyRequest(page); + await proxy.navigate(); + }); + + const baseConfig = /** @type IOAuth2Authorization */ ({ + grantType: 'authorization_code', + clientId: 'auth-code-cid', + clientSecret: 'auth-code-cs', + authorizationUri: 'http://localhost:8000/oauth2/auth-code', + accessTokenUri: 'http://localhost:8000/oauth2/token', + redirectUri: 'http://localhost:8000/test/authorization/popup.html', + scopes: ['a', 'b'], + state: 'my-state', + }); + + test('returns the token info', async () => { + const config = { + ...baseConfig, + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.state).toEqual('my-state'); + expect(tokenInfo.accessToken).toEqual('token1234'); + expect(tokenInfo.tokenType).toEqual('Bearer'); + expect(tokenInfo.refreshToken).toEqual('refresh1234'); + expect(tokenInfo.expiresIn).toStrictEqual(3600); + expect(tokenInfo.scope).toEqual(baseConfig.scopes); + expect(tokenInfo).toHaveProperty('expiresAt'); + expect(tokenInfo.expiresAssumed).toBe(false); + }); + + test('returns error when client_id error', async () => { + const config = { + ...baseConfig, + clientId: 'invalid' + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('Client authentication failed.'); + expect(tokenInfo.code).toEqual('invalid_client'); + expect(tokenInfo.state).toEqual('my-state'); + expect(tokenInfo.error).toEqual(true); + }); + + test('handles when invalid redirect', async () => { + const config = { + ...baseConfig, + redirectUri: new URL('/test/authorization/wrong-redirect.html', 'http://localhost:8000').toString(), + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('invalid redirect'); + expect(tokenInfo.code).toEqual('invalid_request'); + expect(tokenInfo.state).toEqual('my-state'); + expect(tokenInfo.error).toEqual(true); + }); + + test('handles when invalid access token URI', async () => { + const config = { + ...baseConfig, + accessTokenUri: new URL('/invalid', 'http://localhost:8000').toString(), + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('Couldn\'t connect to the server. Authorization URI is invalid. Received status 404.'); + expect(tokenInfo.code).toEqual('request_error'); + expect(tokenInfo.state).toEqual('my-state'); + expect(tokenInfo.error).toEqual(true); + }); + + test('handles no body in token response', async () => { + const config = { + ...baseConfig, + accessTokenUri: new URL('/empty-response', 'http://localhost:8000').toString(), + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('Couldn\'t connect to the server. Code response body is empty.'); + expect(tokenInfo.code).toEqual('request_error'); + expect(tokenInfo.state).toEqual('my-state'); + expect(tokenInfo.error).toEqual(true); + }); + + test('handles custom data', async () => { + const config = /** @type IOAuth2Authorization */ ({ + ...baseConfig, + authorizationUri: 'http://localhost:8000/oauth2/auth-code-custom', + accessTokenUri: 'http://localhost:8000/oauth2/token-custom', + customData: { + auth: { + parameters: [{ + name: 'customQuery', + value: 'customQueryValue' + }] + }, + token: { + body: [{ + name: 'customBody', + value: 'customBodyValue' + }], + headers: [{ + name: 'customHeader', + value: 'customHeaderValue' + }], + parameters: [{ + name: 'customParameter', + value: 'customParameterValue' + }], + }, + } + }); + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.accessToken).toEqual('token1234'); + }); + + test('returns the token for PKCE extension', async () => { + // during this test the mock server actually performs the check for the challenge and the verifier + const config = /** @type IOAuth2Authorization */ ({ + ...baseConfig, + pkce: true, + }); + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.accessToken).toEqual('token1234'); + }); + + test('returns error when PKCE verification fails', async () => { + // during this test the mock server actually performs the check for the challenge and the verifier + const config = /** @type IOAuth2Authorization */ ({ + ...baseConfig, + pkce: true, + customData: { + auth: { + parameters: [{ + name: 'failPkce', + value: 'true' + }] + }, + }, + }); + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('invalid code_verifier'); + expect(tokenInfo.code).toEqual('invalid_request'); + expect(tokenInfo.state).toEqual('my-state'); + expect(tokenInfo.error).toEqual(true); + }); + }); +}); diff --git a/test/oauth2-custom-proxy.test.js b/test/oauth2-custom-proxy.test.js new file mode 100644 index 0000000..e1dc3c5 --- /dev/null +++ b/test/oauth2-custom-proxy.test.js @@ -0,0 +1,125 @@ +import { test as base, chromium, expect } from '@playwright/test'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { ProxyRequest } from './models/ProxyRequest.js'; + +/** @typedef {import('../src/types').IOAuth2Authorization} IOAuth2Authorization */ + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const extensionPath = join(__dirname, '..'); + +const test = base.extend({ + context: async ({ browserName }, use) => { + const browserTypes = { chromium }; + const launchOptions = { + devtools: true, + headless: false, + args: [ + `--disable-extensions-except=${extensionPath}` + ], + viewport: { + width: 1920, + height: 1080 + }, + }; + const context = await browserTypes[browserName].launchPersistentContext('', launchOptions); + await use(context); + await context.close(); + } +}); + +test.describe('OAuth 2.0 Proxy', () => { + test.describe('custom grant', () => { + /** @type ProxyRequest */ + let proxy; + test.beforeEach(async ({ page }) => { + proxy = new ProxyRequest(page); + await proxy.navigate(); + }); + + const baseConfig = /** @type IOAuth2Authorization */ ({ + grantType: 'custom-grant', + username: 'test-uname', + password: 'test-passwd', + clientId: 'auth-code-cid', + scopes: ['a', 'b'], + accessTokenUri: 'http://localhost:8000/oauth2/custom-grant', + }); + + test('returns the token info', async () => { + const config = { + ...baseConfig, + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.accessToken).toEqual('token1234'); + expect(tokenInfo.tokenType).toEqual('Bearer'); + expect(tokenInfo.refreshToken).toEqual('refresh1234'); + expect(tokenInfo.expiresIn).toStrictEqual(3600); + expect(tokenInfo.scope).toEqual(baseConfig.scopes); + expect(tokenInfo).toHaveProperty('expiresAt'); + expect(tokenInfo.expiresAssumed).toBe(false); + }); + + test('returns error when invalid client_id', async () => { + const config = { + ...baseConfig, + clientId: 'invalid' + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('invalid client id'); + expect(tokenInfo.code).toEqual('invalid_client'); + expect(tokenInfo.error).toEqual(true); + }); + + test('reports server scopes', async () => { + const config = { + ...baseConfig, + }; + delete config.scopes; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.scope).toEqual(['custom']); + }); + + test('handles invalid grant', async () => { + const config = { + ...baseConfig, + grantType: 'other-custom', + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.code).toEqual('invalid_grant'); + expect(tokenInfo.error).toEqual(true); + }); + + test('handles custom data', async () => { + const config = { + ...baseConfig, + clientId: 'custom-data', + customData: { + auth: { + parameters: [{ + name: 'customQuery', + value: 'customQueryValue' + }] + }, + token: { + body: [{ + name: 'customBody', + value: 'customBodyValue' + }], + headers: [{ + name: 'customHeader', + value: 'customHeaderValue' + }], + parameters: [{ + name: 'customParameter', + value: 'customParameterValue' + }], + }, + }, + }; + delete config.scopes; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.accessToken).toEqual('token1234'); + }); + }); +}); diff --git a/test/oauth2-implicit-proxy.test.js b/test/oauth2-implicit-proxy.test.js new file mode 100644 index 0000000..6cd02cf --- /dev/null +++ b/test/oauth2-implicit-proxy.test.js @@ -0,0 +1,97 @@ +import { test as base, chromium, expect } from '@playwright/test'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { ProxyRequest } from './models/ProxyRequest.js'; + +/** @typedef {import('../src/types').IOAuth2Authorization} IOAuth2Authorization */ + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const extensionPath = join(__dirname, '..'); + +const test = base.extend({ + context: async ({ browserName }, use) => { + const browserTypes = { chromium }; + const launchOptions = { + devtools: true, + headless: false, + args: [ + `--disable-extensions-except=${extensionPath}` + ], + viewport: { + width: 1920, + height: 1080 + }, + }; + const context = await browserTypes[browserName].launchPersistentContext('', launchOptions); + await use(context); + await context.close(); + } +}); + +test.describe('OAuth 2.0 Proxy', () => { + test.describe('implicit grant', () => { + /** @type ProxyRequest */ + let proxy; + test.beforeEach(async ({ page }) => { + proxy = new ProxyRequest(page); + await proxy.navigate(); + }); + + const baseConfig = /** @type IOAuth2Authorization */ ({ + grantType: 'implicit', + clientId: 'auth-code-cid', + authorizationUri: 'http://localhost:8000/oauth2/auth-implicit', + redirectUri: 'http://localhost:8000/test/authorization/popup.html', + scopes: ['a', 'b'], + state: 'my-state', + }); + + test('returns the token info', async () => { + const config = { + ...baseConfig, + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.state).toEqual('my-state'); + expect(tokenInfo.accessToken).toEqual('token1234'); + expect(tokenInfo.tokenType).toEqual('Bearer'); + expect(tokenInfo.refreshToken).toEqual('refresh1234'); + expect(tokenInfo.expiresIn).toStrictEqual(3600); + expect(tokenInfo.scope).toEqual(baseConfig.scopes); + expect(tokenInfo).toHaveProperty('expiresAt'); + expect(tokenInfo.expiresAssumed).toBe(false); + }); + + test('has scopes returned by the server', async () => { + const config = { + ...baseConfig, + clientId: 'custom-scopes', + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.scope).toEqual(['c1', 'c2']); + }); + + test('returns error when client_id error', async () => { + const config = { + ...baseConfig, + clientId: 'invalid' + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('Client authentication failed.'); + expect(tokenInfo.code).toEqual('invalid_client'); + expect(tokenInfo.state).toEqual('my-state'); + expect(tokenInfo.error).toEqual(true); + }); + + test('handles when state is different', async () => { + const config = { + ...baseConfig, + authorizationUri: new URL('/oauth2/auth-implicit-invalid-state', 'http://localhost:8000').toString(), + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('The state value returned by the authorization server is invalid.'); + expect(tokenInfo.code).toEqual('invalid_state'); + expect(tokenInfo.state).toEqual('my-state'); + expect(tokenInfo.error).toEqual(true); + }); + }); +}); diff --git a/test/oauth2-password-proxy.test.js b/test/oauth2-password-proxy.test.js new file mode 100644 index 0000000..933468b --- /dev/null +++ b/test/oauth2-password-proxy.test.js @@ -0,0 +1,126 @@ +import { test as base, chromium, expect } from '@playwright/test'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { ProxyRequest } from './models/ProxyRequest.js'; + +/** @typedef {import('../src/types').IOAuth2Authorization} IOAuth2Authorization */ + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const extensionPath = join(__dirname, '..'); + +const test = base.extend({ + context: async ({ browserName }, use) => { + const browserTypes = { chromium }; + const launchOptions = { + devtools: true, + headless: false, + args: [ + `--disable-extensions-except=${extensionPath}` + ], + viewport: { + width: 1920, + height: 1080 + }, + }; + const context = await browserTypes[browserName].launchPersistentContext('', launchOptions); + await use(context); + await context.close(); + } +}); + +test.describe('OAuth 2.0 Proxy', () => { + test.describe('password grant', () => { + /** @type ProxyRequest */ + let proxy; + test.beforeEach(async ({ page }) => { + proxy = new ProxyRequest(page); + await proxy.navigate(); + }); + + const baseConfig = /** @type IOAuth2Authorization */ ({ + grantType: 'password', + username: 'test-uname', + password: 'test-passwd', + clientId: 'auth-code-cid', + scopes: ['a', 'b'], + accessTokenUri: 'http://localhost:8000/oauth2/password', + }); + + test('returns the token info', async () => { + const config = { + ...baseConfig, + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.accessToken).toEqual('token1234'); + expect(tokenInfo.tokenType).toEqual('Bearer'); + expect(tokenInfo.refreshToken).toEqual('refresh1234'); + expect(tokenInfo.expiresIn).toStrictEqual(3600); + expect(tokenInfo.scope).toEqual(baseConfig.scopes); + expect(tokenInfo).toHaveProperty('expiresAt'); + expect(tokenInfo.expiresAssumed).toBe(false); + }); + + test('returns error when invalid client_id', async () => { + const config = { + ...baseConfig, + clientId: 'invalid' + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('invalid client id'); + expect(tokenInfo.code).toEqual('invalid_client'); + expect(tokenInfo.error).toEqual(true); + }); + + test('handles when invalid credentials', async () => { + const config = { + ...baseConfig, + username: 'invalid', + }; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.message).toEqual('invalid username'); + expect(tokenInfo.code).toEqual('invalid_client'); + expect(tokenInfo.error).toEqual(true); + }); + + test('reports server scopes', async () => { + const config = { + ...baseConfig, + }; + delete config.scopes; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.scope).toEqual(['custom']); + }); + + test('handles custom data', async () => { + const config = { + ...baseConfig, + clientId: 'custom-data', + customData: { + auth: { + parameters: [{ + name: 'customQuery', + value: 'customQueryValue' + }] + }, + token: { + body: [{ + name: 'customBody', + value: 'customBodyValue' + }], + headers: [{ + name: 'customHeader', + value: 'customHeaderValue' + }], + parameters: [{ + name: 'customParameter', + value: 'customParameterValue' + }], + }, + } + }; + delete config.scopes; + const tokenInfo = await proxy.proxyOauth2(config); + expect(tokenInfo.accessToken).toEqual('token1234'); + }); + }); +}); diff --git a/web-dev-server.config.mjs b/web-dev-server.config.mjs index 33286f0..ad27e35 100644 --- a/web-dev-server.config.mjs +++ b/web-dev-server.config.mjs @@ -1,5 +1,6 @@ // import { hmrPlugin, presets } from '@open-wc/dev-server-hmr'; // import { esbuildPlugin } from '@web/dev-server-esbuild'; +import { CodeServerMock } from './test/authorization/ServerMock.js'; export default /** @type {import('@web/dev-server').DevServerConfig} */ ({ // open: '/demo/', @@ -22,9 +23,55 @@ export default /** @type {import('@web/dev-server').DevServerConfig} */ ({ // appIndex: 'demo/index.html', plugins: [ + { + name: 'mock-api', + serve(context) { + if (context.path === '/oauth2/auth-code') { + return CodeServerMock.authRequest(context.request); + } + if (context.path === '/oauth2/token') { + return CodeServerMock.tokenRequest(context); + } + if (context.path === '/oauth2/auth-code-custom') { + return CodeServerMock.authRequestCustom(context.request); + } + if (context.path === '/oauth2/token-custom') { + return CodeServerMock.tokenRequestCustom(context); + } + if (context.path === '/oauth2/password') { + return CodeServerMock.tokenPassword(context); + } + if (context.path === '/oauth2/client-credentials') { + return CodeServerMock.tokenClientCredentials(context); + } + if (context.path === '/oauth2/client-credentials-header') { + return CodeServerMock.tokenClientCredentialsHeader(context); + } + if (context.path === '/oauth2/custom-grant') { + return CodeServerMock.tokenCustomGrant(context); + } + if (context.path === '/empty-response') { + return ''; + } + return undefined; + }, + }, ], // preserveSymlinks: true, - // See documentation for all available options + middleware: [ + function implicitAuth(context, next) { + if (context.path === '/oauth2/auth-implicit') { + return CodeServerMock.authRequestImplicit(context); + } + if (context.path === '/oauth2/auth-implicit-custom') { + return CodeServerMock.authRequestImplicitCustom(context); + } + if (context.path === '/oauth2/auth-implicit-invalid-state') { + return CodeServerMock.authRequestImplicitStateError(context); + } + return next(); + } + ], });