webpods
Version:
Append-only log service with OAuth authentication
156 lines • 5.35 kB
JavaScript
/**
* Provider-agnostic OAuth implementation
*/
import { generators } from "openid-client";
import { getOAuthClient, getProviderConfig, isProviderConfigured, } from "./oauth-config.js";
/**
* Generate PKCE challenge
*/
export function generatePKCE() {
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);
return { codeVerifier, codeChallenge };
}
/**
* Generate state for OAuth flow
*/
export function generateState() {
return generators.state();
}
/**
* Get authorization URL for any provider
*/
export async function getAuthorizationUrl(providerId, state, codeChallenge) {
if (!isProviderConfigured(providerId)) {
throw new Error(`OAuth provider ${providerId} not configured`);
}
const client = await getOAuthClient(providerId);
const config = getProviderConfig(providerId);
const params = {
state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
scope: config.scope,
};
return client.authorizationUrl(params);
}
/**
* Exchange authorization code for tokens
*/
export async function exchangeCodeForTokens(providerId, code, codeVerifier) {
const client = await getOAuthClient(providerId);
const config = getProviderConfig(providerId);
// Handle localhost OAuth providers (for testing)
if (config.issuer?.includes("localhost")) {
const tokenSet = await client.grant({
grant_type: "authorization_code",
code,
redirect_uri: config.redirectUri,
code_verifier: codeVerifier,
});
return tokenSet;
}
const tokenSet = await client.callback(config.redirectUri, { code }, { code_verifier: codeVerifier });
return tokenSet;
}
/**
* Get user info from any provider
*/
export async function getUserInfo(providerId, accessToken) {
const client = await getOAuthClient(providerId);
const config = getProviderConfig(providerId);
// If provider has a custom userinfo URL, use it directly
if (!config.issuer && config.userinfoUrl) {
return await getCustomUserInfo(accessToken, config);
}
// Standard OIDC userinfo endpoint
try {
const userinfo = await client.userinfo(accessToken);
return normalizeUserInfo(userinfo, config);
}
catch (error) {
// Fallback to manual API call if userinfo fails
if (config.userinfoUrl) {
const response = await fetch(config.userinfoUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`Failed to get user info from ${providerId}`);
}
const data = await response.json();
return normalizeUserInfo(data, config);
}
throw error;
}
}
/**
* Get user info from custom userinfo endpoint
*/
async function getCustomUserInfo(accessToken, config) {
if (!config.userinfoUrl) {
throw new Error(`Provider ${config.id} missing userinfo URL`);
}
const response = await fetch(config.userinfoUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`Failed to get user info from ${config.id}`);
}
const data = await response.json();
// Some providers may have a separate email endpoint
// This can be configured in config.json if needed
if (config.emailUrl && !data[config.emailField]) {
const emailResponse = await fetch(config.emailUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
});
if (emailResponse.ok) {
const emailData = await emailResponse.json();
// Handle array of emails (some providers return multiple)
if (Array.isArray(emailData)) {
const primaryEmail = emailData.find((e) => e.primary || e.verified);
if (primaryEmail) {
data[config.emailField] = primaryEmail.email || primaryEmail.value;
}
}
else {
data[config.emailField] =
emailData[config.emailField] || emailData.email;
}
}
}
return normalizeUserInfo(data, config);
}
/**
* Normalize user info based on provider config field mappings
*/
function normalizeUserInfo(data, config) {
// Extract fields based on config mappings
const userId = data[config.userIdField] || data.id || data.sub;
const email = data[config.emailField] || data.email;
const name = data[config.nameField] || data.name || data.username || data.login;
// Standard normalized format
return {
id: userId,
email: email,
name: name,
username: data.username || data.login,
picture: data.picture || data.avatar_url || data.avatar,
raw: data, // Include raw data for debugging
};
}
/**
* Validate that a provider is supported
*/
export function validateProvider(providerId) {
return isProviderConfigured(providerId);
}
//# sourceMappingURL=oauth-handlers.js.map