Skip to content

Commit

Permalink
tunnel-server: redirect to login even without SaaS (#406)
Browse files Browse the repository at this point in the history
  • Loading branch information
Roy Razon authored Jan 28, 2024
1 parent d019836 commit 43914e4
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 49 deletions.
14 changes: 3 additions & 11 deletions tunnel-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 })
Expand All @@ -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 = (
Expand Down
33 changes: 22 additions & 11 deletions tunnel-server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Claims>
activeTunnelStore: ActiveTunnelStore
proxy: Proxy
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
}
3 changes: 1 addition & 2 deletions tunnel-server/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 }),
}
}
Expand Down
33 changes: 8 additions & 25 deletions tunnel-server/src/proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -31,16 +22,14 @@ export const proxy = ({
sessionStore,
log,
authFactory,
loginConfig,
loginUrl,
}: {
sessionStore: SessionStore<Claims>
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 })
Expand All @@ -53,9 +42,9 @@ export const proxy = ({
session: ReturnType<typeof sessionStore>,
) => {
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)
Expand All @@ -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') {
Expand Down

0 comments on commit 43914e4

Please sign in to comment.