diff --git a/tunnel-server/index.ts b/tunnel-server/index.ts index 66b5203a..f3ee0bf9 100644 --- a/tunnel-server/index.ts +++ b/tunnel-server/index.ts @@ -2,7 +2,7 @@ import { promisify } from 'util' import pino from 'pino' import fs from 'fs' import { KeyObject, createPublicKey } from 'crypto' -import { app as createApp } from './src/app.js' +import { buildLoginUrl, app as createApp } from './src/app.js' import { activeTunnelStoreKey, inMemoryActiveTunnelStore } from './src/tunnel-store/index.js' import { getSSHKeys } from './src/ssh-keys.js' import { proxy } from './src/proxy/index.js' @@ -64,19 +64,11 @@ if (saasIdp) { const baseIdentityProviders: readonly IdentityProvider[] = Object.freeze(saasIdp ? [saasIdp] : []) -const loginConfig = saasIdp - ? { - loginUrl: new URL('/login', editUrl(BASE_URL, { hostname: `auth.${BASE_URL.hostname}` })).toString(), - saasBaseUrl: requiredEnv('SAAS_BASE_URL'), - } - : undefined - const authFactory = ( { publicKey, publicKeyThumbprint }: { publicKey: KeyObject; publicKeyThumbprint: string }, ) => jwtAuthenticator( publicKeyThumbprint, baseIdentityProviders.concat(cliIdentityProvider(publicKey, publicKeyThumbprint)), - loginConfig !== undefined, ) const activeTunnelStore = inMemoryActiveTunnelStore({ log }) @@ -92,11 +84,11 @@ const app = createApp({ sessionStore, baseHostname: BASE_URL.hostname, authFactory, - loginConfig, + loginUrl: ({ env, returnPath }) => buildLoginUrl({ baseUrl: BASE_URL, env, returnPath }), }), log, authFactory, - loginConfig, + saasBaseUrl: saasIdp ? requiredEnv('SAAS_BASE_URL') : undefined, }) const tunnelUrl = ( diff --git a/tunnel-server/src/app.ts b/tunnel-server/src/app.ts index 5f408073..f3d3ac9e 100644 --- a/tunnel-server/src/app.ts +++ b/tunnel-server/src/app.ts @@ -9,14 +9,24 @@ import { ActiveTunnelStore } from './tunnel-store/index.js' import { editUrl } from './url.js' import { Proxy } from './proxy/index.js' +export const buildLoginUrl = ({ baseUrl, env, returnPath }: { + baseUrl: URL + env: string + returnPath?: string +}) => editUrl(baseUrl, { + hostname: `auth.${baseUrl.hostname}`, + queryParams: { + env, + ...returnPath ? { returnPath } : {}, + }, + path: '/login', +}).toString() + export const app = ( - { proxy, sessionStore, baseUrl, activeTunnelStore, log, loginConfig, authFactory }: { + { proxy, sessionStore, baseUrl, activeTunnelStore, log, saasBaseUrl, authFactory }: { log: Logger baseUrl: URL - loginConfig?: { - loginUrl: string - saasBaseUrl: string - } + saasBaseUrl?: string sessionStore: SessionStore activeTunnelStore: ActiveTunnelStore proxy: Proxy @@ -87,9 +97,7 @@ export const app = ( .get('/healthz', { logLevel: 'warn' }, async () => 'OK') - if (loginConfig) { - const { loginUrl, saasBaseUrl } = loginConfig - a.get<{Querystring: {env: string; returnPath?: string}}>('/login', { + .get<{Querystring: {env: string; returnPath?: string}}>('/login', { schema: { querystring: { type: 'object', @@ -117,15 +125,18 @@ export const app = ( const auth = authFactory(activeTunnel) const result = await auth(req.raw) if (!result.isAuthenticated) { - return await res.header('Access-Control-Allow-Origin', saasBaseUrl) - .redirect(`${saasBaseUrl}/api/auth/login?redirectTo=${encodeURIComponent(`${loginUrl}?env=${envId}&returnPath=${returnPath}`)}`) + if (saasBaseUrl) { + return await res.header('Access-Control-Allow-Origin', saasBaseUrl) + .redirect(`${saasBaseUrl}/api/auth/login?redirectTo=${encodeURIComponent(buildLoginUrl({ baseUrl, env: envId, returnPath }))}`) + } + res.statusCode = 401 + return { error: 'Unauthorized' } } session.set(result.claims) session.save() } return await res.redirect(new URL(returnPath, editUrl(baseUrl, { hostname: `${envId}.${baseUrl.hostname}` })).toString()) }) - } return a } diff --git a/tunnel-server/src/auth.ts b/tunnel-server/src/auth.ts index 9b203358..4443991e 100644 --- a/tunnel-server/src/auth.ts +++ b/tunnel-server/src/auth.ts @@ -87,7 +87,6 @@ const extractAuthorizationHeader = (req: IncomingMessage): AuthorizationHeader | export const jwtAuthenticator = ( publicKeyThumbprint: string, identityProviders: IdentityProvider[], - loginEnabled: boolean, ) : Authenticator => async req => { const authHeader = extractAuthorizationHeader(req) const jwt = match(authHeader) @@ -120,7 +119,7 @@ export const jwtAuthenticator = ( return { method: { type: 'header', header: 'authorization' }, isAuthenticated: true, - login: loginEnabled && isBrowser(req) && authHeader?.scheme !== 'Bearer', + login: isBrowser(req) && authHeader?.scheme !== 'Bearer', claims: mapClaims(token.payload, { pkThumbprint: publicKeyThumbprint }), } } diff --git a/tunnel-server/src/proxy/index.ts b/tunnel-server/src/proxy/index.ts index e0cf8a4c..57dee23f 100644 --- a/tunnel-server/src/proxy/index.ts +++ b/tunnel-server/src/proxy/index.ts @@ -13,15 +13,6 @@ import { BadGatewayError, BadRequestError, BasicAuthUnauthorizedError, RedirectE import { TunnelFinder, proxyRouter } from './router.js' import { proxyInjectionHandlers } from './injection/index.js' -const loginRedirectUrl = ({ loginUrl, env, returnPath }: { loginUrl: string; env: string; returnPath?: string }) => { - const url = new URL(loginUrl) - url.searchParams.set('env', env) - if (returnPath) { - url.searchParams.set('returnPath', returnPath) - } - return url.toString() -} - const hasBasicAuthQueryParamHint = (url: string) => new URL(url, 'http://a').searchParams.get('_preevy_auth_hint') === 'basic' @@ -31,16 +22,14 @@ export const proxy = ({ sessionStore, log, authFactory, - loginConfig, + loginUrl, }: { sessionStore: SessionStore activeTunnelStore: ActiveTunnelStore baseHostname: string log: Logger authFactory: (client: { publicKey: KeyObject; publicKeyThumbprint: string }) => Authenticator - loginConfig?: { - loginUrl: string - } + loginUrl: ({ env, returnPath }: { env: string; returnPath?: string }) => string }) => { const theProxy = httpProxy.createProxyServer({ xfwd: true }) const injectionHandlers = proxyInjectionHandlers({ log }) @@ -53,9 +42,9 @@ export const proxy = ({ session: ReturnType, ) => { if (!session.user) { - const redirectToLoginError = ({ loginUrl }: { loginUrl: string }) => new RedirectError( + const redirectToLoginError = () => new RedirectError( 307, - loginRedirectUrl({ loginUrl, env: tunnel.hostname, returnPath: req.url }), + loginUrl({ env: tunnel.hostname, returnPath: req.url }), ) const authenticate = authFactory(tunnel) @@ -72,21 +61,15 @@ export const proxy = ({ } if (!authResult.isAuthenticated) { - if (req.url !== undefined && hasBasicAuthQueryParamHint(req.url)) { - throw new BasicAuthUnauthorizedError() - } - - throw loginConfig ? redirectToLoginError(loginConfig) : new UnauthorizedError() + throw req.url !== undefined && hasBasicAuthQueryParamHint(req.url) + ? new BasicAuthUnauthorizedError() + : redirectToLoginError() } session.set(authResult.claims) if (authResult.login && req.method === 'GET' && !req.headers.upgrade) { - if (!loginConfig) { - log.error('Login requested for %j, but login is not configured', authResult.claims) - throw new UnauthorizedError() - } session.save() - throw redirectToLoginError(loginConfig) + throw redirectToLoginError() } if (authResult.method.type === 'header') {