@redocly/portal-plugin-gravitee-sso
Version:
Gravitee SSO plugin for @redocly/portal
127 lines (97 loc) • 4.19 kB
text/typescript
import { setCookie } from 'hono/cookie';
import { DEFAULT_COOKIE_EXPIRATION } from '@redocly/realm/dist/shared/constants.js';
import { JWT_SECRET_KEY } from '@redocly/realm/dist/server/constants.js';
import * as jwt from '@redocly/realm/dist/server/web-server/jwt/jwt.js';
import type { Store } from '@redocly/realm/dist/server/store.js';
import type { Logger } from '@redocly/realm/dist/server/utils/reporter/logger.js';
import type { Context } from 'hono';
import type { GraviteeSsoConfig } from './config.js';
import { getPathPrefix, withPathPrefix } from '@redocly/theme/core/utils';
import { isGraviteeSsoConfig } from './is-gravitee-sso-config.js';
import { GraviteeSsoError, LOGIN_PAGE_SLUG } from './constants.js';
export default async function (ctx: Context, store: Store): Promise<Response> {
const logger = ctx.get('logger') as Logger;
const parsedUrl = new URL(ctx.req.url);
const idpId = parsedUrl.searchParams.get('idpId');
const redirectTo = parsedUrl.searchParams.get('redirectTo') || '/';
const loginPagePathname =
parsedUrl.searchParams.get('loginPagePathname') || withPathPrefix(LOGIN_PAGE_SLUG);
if (!idpId) {
logger.error('Gravitee SSO: idpId query parameter not found');
return ctx.text('Forbidden', 403);
}
const createErrorResponse = (error: GraviteeSsoError) => {
const errorSearchParams = new URLSearchParams({
error,
idpId,
redirectTo,
});
const errorRedirectUrl = `${loginPagePathname}?${errorSearchParams}`;
return ctx.newResponse(null, 302, { Location: errorRedirectUrl });
};
const idpConfig = store.getConfig().ssoDirect?.[idpId] as GraviteeSsoConfig | unknown;
if (!isGraviteeSsoConfig(idpConfig)) {
logger.error(`Gravitee SSO: invalid idpId in query params: ${idpId}`);
return ctx.text('Forbidden', 403);
}
const formData = await ctx.req.formData();
const username = formData.get('username');
const password = formData.get('password');
if (!username || !password) {
return createErrorResponse(GraviteeSsoError.INVALID_CREDENTIALS);
}
const basicAuthToken = btoa(username + ':' + password);
const apiUrl = `${idpConfig.apiBaseUrl}/portal/environments/${idpConfig.env || 'DEFAULT'}`;
try {
const authRes = await fetch(`${apiUrl}/auth/login`, {
method: 'POST',
headers: {
Authorization: `Basic ${basicAuthToken}`,
},
});
if (authRes.status === 401) {
logger.verbose('Gravitee SSO callback: invalid username or password');
return createErrorResponse(GraviteeSsoError.INVALID_CREDENTIALS);
} else if (authRes.status !== 200) {
logger.error('Gravitee SSO callback: failed to fetch token from Gravitee API');
return createErrorResponse(GraviteeSsoError.UNKNOWN_ERROR);
}
const userRes = await fetch(`${apiUrl}/user`, {
method: 'GET',
headers: {
Authorization: `Basic ${basicAuthToken}`,
},
});
if (userRes.status !== 200) {
logger.error('Gravitee SSO callback: failed to fetch user information from Gravitee API');
return createErrorResponse(GraviteeSsoError.UNKNOWN_ERROR);
}
const auth = await authRes.json();
const user = await userRes.json();
const userClaims = {
...user,
sub: user.id,
name: user.display_name || user.email || 'Unknown',
idpId,
};
const encodedToken = await jwt.sign(userClaims, JWT_SECRET_KEY);
const expirationDate = Date.now() + DEFAULT_COOKIE_EXPIRATION * 1000;
setCookie(ctx, 'idp_access_token', auth.token || '', {
path: getPathPrefix() || '/',
httpOnly: true,
expires: new Date(expirationDate),
});
const c1 = (ctx as any)._headers['set-cookie'];
setCookie(ctx, 'authorization', encodedToken, {
path: getPathPrefix() || '/',
httpOnly: true,
expires: new Date(expirationDate),
});
const resp = ctx.newResponse(null, 302, { Location: withPathPrefix(redirectTo) });
resp.headers.append('set-cookie', c1);
return resp;
} catch (error) {
logger.error(`Gravitee sso callback: ${error.message}`);
return createErrorResponse(GraviteeSsoError.UNKNOWN_ERROR);
}
}