signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
146 lines • 5.45 kB
JavaScript
;
/*
* Copyright 2025 Matti Airas
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.setFetchFunction = setFetchFunction;
exports.resetFetchFunction = resetFetchFunction;
exports.exchangeAuthorizationCode = exchangeAuthorizationCode;
exports.fetchUserinfo = fetchUserinfo;
const types_1 = require("./types");
let fetchFn = fetch;
/**
* Set the fetch function (for testing)
*/
function setFetchFunction(fn) {
fetchFn = fn;
}
/**
* Reset the fetch function to the default
*/
function resetFetchFunction() {
fetchFn = fetch;
}
/**
* Exchange an authorization code for tokens
* @param code The authorization code from the callback
* @param config OIDC configuration
* @param metadata Discovery document metadata
* @param authState The auth state containing code verifier
* @returns The token response
* @throws OIDCError if the exchange fails
*/
async function exchangeAuthorizationCode(code, config, metadata, authState) {
const params = new URLSearchParams();
params.set('grant_type', 'authorization_code');
params.set('code', code);
params.set('redirect_uri', authState.redirectUri);
params.set('client_id', config.clientId);
params.set('client_secret', config.clientSecret);
params.set('code_verifier', authState.codeVerifier);
let response;
try {
response = await fetchFn(metadata.token_endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
}
catch (err) {
throw new types_1.OIDCError(`Token exchange request failed: ${err}`, 'TOKEN_EXCHANGE_FAILED', err instanceof Error ? err : undefined);
}
let body;
try {
body = (await response.json());
}
catch (err) {
throw new types_1.OIDCError('Failed to parse token response as JSON', 'TOKEN_EXCHANGE_FAILED', err instanceof Error ? err : undefined);
}
if (!response.ok) {
const errorMessage = body.error_description || body.error || 'Unknown error';
throw new types_1.OIDCError(`Token exchange failed: ${body.error} - ${errorMessage}`, 'TOKEN_EXCHANGE_FAILED');
}
// Validate response structure
if (!body.access_token || !body.id_token) {
throw new types_1.OIDCError('Token response missing required fields (access_token, id_token)', 'TOKEN_EXCHANGE_FAILED');
}
return {
accessToken: body.access_token,
idToken: body.id_token,
refreshToken: body.refresh_token,
expiresIn: body.expires_in,
tokenType: body.token_type || 'Bearer'
};
}
/**
* Fetch additional claims from the userinfo endpoint
*
* The ID token typically only contains minimal claims (sub, iss, aud, etc.).
* Claims like groups, email, name are returned via the userinfo endpoint.
*
* @param accessToken The access token from the token response
* @param metadata Discovery document metadata containing userinfo_endpoint
* @param issuer The configured OIDC issuer URL (for hostname validation)
* @returns Additional claims from the userinfo endpoint, or undefined if unavailable
*/
async function fetchUserinfo(accessToken, metadata, issuer) {
// Not all providers have a userinfo endpoint
if (!metadata.userinfo_endpoint) {
return undefined;
}
// Security: validate userinfo endpoint hostname matches issuer hostname
// This prevents a malicious discovery document from redirecting userinfo
// requests to an attacker-controlled server
try {
const issuerHost = new URL(issuer).hostname;
const userinfoHost = new URL(metadata.userinfo_endpoint).hostname;
if (issuerHost !== userinfoHost) {
console.warn(`OIDC: Userinfo endpoint hostname (${userinfoHost}) does not match issuer hostname (${issuerHost})`);
return undefined;
}
}
catch (err) {
console.warn(`OIDC: Failed to validate userinfo endpoint URL: ${err}`);
return undefined;
}
let response;
try {
response = await fetchFn(metadata.userinfo_endpoint, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`
}
});
}
catch (err) {
// Non-fatal: continue with ID token claims only
console.warn(`OIDC: Failed to fetch userinfo: ${err instanceof Error ? err.message : err}`);
return undefined;
}
if (!response.ok) {
console.warn(`OIDC: Userinfo request failed with status ${response.status}`);
return undefined;
}
try {
return (await response.json());
}
catch (err) {
console.warn(`OIDC: Failed to parse userinfo response: ${err instanceof Error ? err.message : err}`);
return undefined;
}
}
//# sourceMappingURL=token-exchange.js.map