diff --git a/core/src/cloud/auth.ts b/core/src/cloud/auth.ts index 6bff54d516..c09de13c61 100644 --- a/core/src/cloud/auth.ts +++ b/core/src/cloud/auth.ts @@ -13,14 +13,15 @@ import type EventEmitter2 from "eventemitter2" import bodyParser from "koa-bodyparser" import Router from "koa-router" import getPort from "get-port" +import cloneDeep from "fast-copy" import type { Log } from "../logger/log-entry.js" -import { cloneDeep, isArray } from "lodash-es" import { gardenEnv } from "../constants.js" import type { GlobalConfigStore } from "../config-store/global.js" import { dedent, deline } from "../util/string.js" import { CloudApiError, InternalError } from "../exceptions.js" import { add } from "date-fns" import { getCloudDistributionName } from "./util.js" +import type { ParsedUrlQuery } from "node:querystring" export interface AuthToken { token: string @@ -117,23 +118,23 @@ export const authTokenHeader = export const makeAuthHeader = (clientAuthToken: string) => ({ [authTokenHeader]: clientAuthToken }) +export type AuthRedirectServerConfig = { + events: EventEmitter2.EventEmitter2 + log: Log + getLoginUrl: (port: number) => string + successUrl: string + extractAuthToken: (query: ParsedUrlQuery) => AuthToken +} + // TODO: Add analytics tracking export class AuthRedirectServer { - private log: Log + private readonly log: Log + private server?: Server private app?: Koa - private enterpriseDomain: string - private events: EventEmitter2.EventEmitter2 - - constructor( - enterpriseDomain: string, - events: EventEmitter2.EventEmitter2, - log: Log, - public port?: number - ) { - this.enterpriseDomain = enterpriseDomain - this.events = events - this.log = log.createLog({}) + + constructor(private readonly config: AuthRedirectServerConfig) { + this.log = config.log.createLog({}) } async start() { @@ -141,13 +142,10 @@ export class AuthRedirectServer { return } - if (!this.port) { - this.port = await getPort() - } + const port = await getPort() - await this.createApp() - const url = new URL(`/clilogin/${this.port}`, this.enterpriseDomain) - await open(url.href) + await this.createApp(port) + await open(this.config.getLoginUrl(port)) } async close() { @@ -160,23 +158,15 @@ export class AuthRedirectServer { return undefined } - async createApp() { + async createApp(port: number) { const app = new Koa() const http = new Router() http.get("/", async (ctx) => { - const { jwt, rt, jwtval } = ctx.request.query - // TODO: validate properly - const tokenResponse: AuthToken = { - token: getFirstValue(jwt!), - refreshToken: getFirstValue(rt!), - tokenValidity: parseInt(getFirstValue(jwtval!), 10), - } + const tokenResponse = this.config.extractAuthToken(ctx.request.query) this.log.debug("Received client auth token") - this.events.emit("receivedToken", tokenResponse) - ctx.redirect(`${this.enterpriseDomain}/clilogin/success`) - const url = new URL("/clilogin/success", this.enterpriseDomain) - ctx.redirect(url.href) + this.config.events.emit("receivedToken", tokenResponse) + ctx.redirect(this.config.successUrl) }) app.use(bodyParser()) @@ -185,10 +175,7 @@ export class AuthRedirectServer { app.on("error", (err) => { this.log.error(`Auth redirect request failed with status ${err.status}: ${err.message}`) }) - this.server = app.listen(this.port) + this.server = app.listen(port) + this.app = app } } - -function getFirstValue(v: string | string[]) { - return isArray(v) ? v[0] : v -} diff --git a/core/src/cloud/backend.ts b/core/src/cloud/backend.ts new file mode 100644 index 0000000000..e8ad92de07 --- /dev/null +++ b/core/src/cloud/backend.ts @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018-2024 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import type { AuthRedirectServerConfig } from "./auth.js" +import { isArray } from "lodash-es" + +function getFirstValue(v: string | string[]) { + return isArray(v) ? v[0] : v +} + +export type GardenBackendConfig = { readonly cloudDomain: string } +export type AuthRedirectConfig = Pick + +export abstract class AbstractGardenBackend { + constructor(protected readonly config: GardenBackendConfig) {} + + abstract getAuthRedirectConfig(): AuthRedirectConfig +} + +export class GardenCloudBackend extends AbstractGardenBackend { + override getAuthRedirectConfig(): AuthRedirectConfig { + return { + getLoginUrl: (port) => new URL(`/clilogin/${port}`, this.config.cloudDomain).href, + successUrl: new URL("/clilogin/success", this.config.cloudDomain).href, + extractAuthToken: (query) => { + const { jwt, rt, jwtval } = query + // TODO: validate properly + return { + token: getFirstValue(jwt!), + refreshToken: getFirstValue(rt!), + tokenValidity: parseInt(getFirstValue(jwtval!), 10), + } + }, + } + } +} diff --git a/core/src/commands/login.ts b/core/src/commands/login.ts index af25b677fc..99e84e49d3 100644 --- a/core/src/commands/login.ts +++ b/core/src/commands/login.ts @@ -12,7 +12,7 @@ import { printHeader } from "../logger/util.js" import dedent from "dedent" import { GardenCloudApi } from "../cloud/api.js" import type { Log } from "../logger/log-entry.js" -import { ConfigurationError, TimeoutError, InternalError, CloudApiError } from "../exceptions.js" +import { CloudApiError, ConfigurationError, InternalError, TimeoutError } from "../exceptions.js" import type { AuthToken } from "../cloud/auth.js" import { AuthRedirectServer, saveAuthToken } from "../cloud/auth.js" import type { EventBus } from "../events/events.js" @@ -22,6 +22,7 @@ import { BooleanParameter } from "../cli/params.js" import { deline } from "../util/string.js" import { gardenEnv } from "../constants.js" import { getCloudDistributionName, getGardenCloudDomain } from "../cloud/util.js" +import { GardenCloudBackend } from "../cloud/backend.js" const loginTimeoutSec = 60 @@ -112,7 +113,13 @@ export class LoginCommand extends Command<{}, Opts> { export async function login(log: Log, cloudDomain: string, events: EventBus) { // Start auth redirect server and wait for its redirect handler to receive the redirect and finish running. - const server = new AuthRedirectServer(cloudDomain, events, log) + const gardenBackend = new GardenCloudBackend({ cloudDomain }) + const server = new AuthRedirectServer({ + events, + log, + ...gardenBackend.getAuthRedirectConfig(), + }) + const distroName = getCloudDistributionName(cloudDomain) log.debug(`Redirecting to ${distroName} login page...`) const response: AuthToken = await new Promise(async (resolve, reject) => {