From 5a2989a71c83247785c5669783499ddf609fa5be Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Tue, 21 Sep 2021 14:50:53 +0000 Subject: [PATCH] =?UTF-8?q?[server]=C2=A0add=20vscode(-insiders)=20ouath2?= =?UTF-8?q?=20clients?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/gitpod-db/package.json | 2 +- .../src/typeorm/auth-code-repository-db.ts | 4 +- .../src/typeorm/entity/db-oauth-auth-code.ts | 4 +- .../gitpod-db/src/typeorm/user-db-impl.ts | 2 +- components/proxy/conf/Caddyfile | 2 +- .../proxy/plugins/corsorigin/cors_origin.go | 18 ++++- components/server/package.json | 2 +- components/server/src/oauth-server/db.ts | 20 ++++++ .../src/oauth-server/oauth-controller.ts | 67 +++++++++---------- yarn.lock | 8 +-- 10 files changed, 79 insertions(+), 50 deletions(-) diff --git a/components/gitpod-db/package.json b/components/gitpod-db/package.json index 843130e26ab5e0..0661adb0090afe 100644 --- a/components/gitpod-db/package.json +++ b/components/gitpod-db/package.json @@ -24,7 +24,7 @@ ], "dependencies": { "@gitpod/gitpod-protocol": "0.1.5", - "@jmondi/oauth2-server": "^1.1.0", + "@jmondi/oauth2-server": "^2.2.2", "mysql": "^2.18.1", "reflect-metadata": "^0.1.13", "the-big-username-blacklist": "^1.5.2", diff --git a/components/gitpod-db/src/typeorm/auth-code-repository-db.ts b/components/gitpod-db/src/typeorm/auth-code-repository-db.ts index f4c9444301b7ff..51683d1781d7f3 100644 --- a/components/gitpod-db/src/typeorm/auth-code-repository-db.ts +++ b/components/gitpod-db/src/typeorm/auth-code-repository-db.ts @@ -33,7 +33,7 @@ export class AuthCodeRepositoryDB implements OAuthAuthCodeRepository { return (await this.getEntityManager()).getRepository(DBOAuthAuthCodeEntry); } - public async getByIdentifier(authCodeCode: string): Promise { + public async getByIdentifier(authCodeCode: string): Promise { const authCodeRepo = await this.getOauthAuthCodeRepo(); let authCodes = await authCodeRepo.find({ code: authCodeCode }); authCodes = authCodes.filter((te) => new Date(te.expiresAt).getTime() > Date.now()); @@ -54,7 +54,7 @@ export class AuthCodeRepositoryDB implements OAuthAuthCodeRepository { scopes: scopes, }; } - public async persist(authCode: OAuthAuthCode): Promise { + public async persist(authCode: DBOAuthAuthCodeEntry): Promise { const authCodeRepo = await this.getOauthAuthCodeRepo(); authCodeRepo.save(authCode); } diff --git a/components/gitpod-db/src/typeorm/entity/db-oauth-auth-code.ts b/components/gitpod-db/src/typeorm/entity/db-oauth-auth-code.ts index 0f31d27d2dac17..4627794ac5c0e6 100644 --- a/components/gitpod-db/src/typeorm/entity/db-oauth-auth-code.ts +++ b/components/gitpod-db/src/typeorm/entity/db-oauth-auth-code.ts @@ -4,7 +4,7 @@ * See License-AGPL.txt in the project root for license information. */ -import { OAuthAuthCode, OAuthClient, OAuthScope } from "@jmondi/oauth2-server"; +import { CodeChallengeMethod, OAuthAuthCode, OAuthClient, OAuthScope } from "@jmondi/oauth2-server"; import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; import { Transformer } from "../transformer"; import { DBUser } from "./db-user"; @@ -38,7 +38,7 @@ export class DBOAuthAuthCodeEntry implements OAuthAuthCode { type: "varchar", length: 10, }) - codeChallengeMethod: string; + codeChallengeMethod: CodeChallengeMethod @Column({ type: "timestamp", diff --git a/components/gitpod-db/src/typeorm/user-db-impl.ts b/components/gitpod-db/src/typeorm/user-db-impl.ts index e96a0a544a354e..dc0f40715eeedf 100644 --- a/components/gitpod-db/src/typeorm/user-db-impl.ts +++ b/components/gitpod-db/src/typeorm/user-db-impl.ts @@ -486,7 +486,7 @@ export class TypeORMUserDBImpl implements UserDB { } else { var user: MaybeUser; if (accessToken.user) { - user = await this.findUserById(accessToken.user.id); + user = await this.findUserById(accessToken.user.id.toString()); } dbToken = { tokenHash, diff --git a/components/proxy/conf/Caddyfile b/components/proxy/conf/Caddyfile index 7deadd9020cf96..fd9a7c6cc4a3a3 100644 --- a/components/proxy/conf/Caddyfile +++ b/components/proxy/conf/Caddyfile @@ -210,7 +210,7 @@ https://{$GITPOD_DOMAIN} { @codesync path /code-sync* handle @codesync { gitpod.cors_origin { - base_domain {$GITPOD_DOMAIN} + any_domain true } import compression diff --git a/components/proxy/plugins/corsorigin/cors_origin.go b/components/proxy/plugins/corsorigin/cors_origin.go index afe1a98811c9e6..603a31737df5a4 100644 --- a/components/proxy/plugins/corsorigin/cors_origin.go +++ b/components/proxy/plugins/corsorigin/cors_origin.go @@ -27,6 +27,7 @@ func init() { // CorsOrigin implements an HTTP handler that generates a valid CORS Origin value type CorsOrigin struct { + AnyDomain bool `json:"any_domain,omitempty"` BaseDomain string `json:"base_domain,omitempty"` Debug bool `json:"debug,omitempty"` } @@ -50,8 +51,14 @@ var ( // ServeHTTP implements caddyhttp.MiddlewareHandler. func (m CorsOrigin) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + var allowedOrigins []string + if m.AnyDomain { + allowedOrigins = []string{"*"} + } else { + allowedOrigins = []string{"*." + m.BaseDomain} + } c := cors.New(cors.Options{ - AllowedOrigins: []string{"*." + m.BaseDomain}, + AllowedOrigins: allowedOrigins, AllowedMethods: allowedMethods, AllowedHeaders: allowedHeaders, ExposedHeaders: exposeHeaders, @@ -84,6 +91,13 @@ func (m *CorsOrigin) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } switch key { + case "any_domain": + b, err := strconv.ParseBool(value) + if err != nil { + return d.Errf("invalid boolean value for subdirective any_domain '%s'", value) + } + + m.AnyDomain = b case "base_domain": m.BaseDomain = value case "debug": @@ -98,7 +112,7 @@ func (m *CorsOrigin) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } } - if m.BaseDomain == "" { + if !m.AnyDomain && m.BaseDomain == "" { return fmt.Errorf("Please configure the base_domain subdirective") } diff --git a/components/server/package.json b/components/server/package.json index 5b487a5667bff8..0117d9da5ab808 100644 --- a/components/server/package.json +++ b/components/server/package.json @@ -39,7 +39,7 @@ "@gitpod/ws-manager": "0.1.5", "@google-cloud/storage": "^5.6.0", "@improbable-eng/grpc-web-node-http-transport": "^0.14.0", - "@jmondi/oauth2-server": "^1.1.0", + "@jmondi/oauth2-server": "^2.2.2", "@octokit/rest": "18.6.1", "@probot/get-private-key": "^1.1.1", "amqplib": "^0.8.0", diff --git a/components/server/src/oauth-server/db.ts b/components/server/src/oauth-server/db.ts index 47ec2cc46059c6..98f0be130f83f8 100644 --- a/components/server/src/oauth-server/db.ts +++ b/components/server/src/oauth-server/db.ts @@ -57,10 +57,30 @@ const jetBrainsGateway: OAuthClient = { ], }; +function createVSCodeClient(protocol: "vscode" | "vscode-insiders"): OAuthClient { + return { + id: protocol + "-" + "gitpod", + name: `VS Code${protocol === "vscode-insiders" ? " Insiders" : ""}: Gitpod extension`, + redirectUris: [protocol + "://gitpod.gitpod-desktop/complete-gitpod-auth"], + allowedGrants: ["authorization_code"], + scopes: [ + { name: "function:getGitpodTokenScopes" }, + { name: "function:getLoggedInUser" }, + { name: "function:accessCodeSyncStorage" }, + { name: "resource:default" }, + ], + }; +} + +const vscode = createVSCodeClient("vscode"); +const vscodeInsiders = createVSCodeClient("vscode-insiders"); + export const inMemoryDatabase: InMemory = { clients: { [localClient.id]: localClient, [jetBrainsGateway.id]: jetBrainsGateway, + [vscode.id]: vscode, + [vscodeInsiders.id]: vscodeInsiders, }, tokens: {}, scopes: {}, diff --git a/components/server/src/oauth-server/oauth-controller.ts b/components/server/src/oauth-server/oauth-controller.ts index efa1f7b0f58de3..f92c5846841620 100644 --- a/components/server/src/oauth-server/oauth-controller.ts +++ b/components/server/src/oauth-server/oauth-controller.ts @@ -8,9 +8,11 @@ import { AuthCodeRepositoryDB } from "@gitpod/gitpod-db/lib/typeorm/auth-code-re import { UserDB } from "@gitpod/gitpod-db/lib/user-db"; import { User } from "@gitpod/gitpod-protocol"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; -import { OAuthException, OAuthRequest, OAuthResponse } from "@jmondi/oauth2-server"; +import { OAuthRequest, OAuthResponse } from "@jmondi/oauth2-server"; +import { handleExpressResponse, handleExpressError } from "@jmondi/oauth2-server/dist/adapters/express"; import * as express from "express"; import { inject, injectable } from "inversify"; +import { URL } from "url"; import { Config } from "../config"; import { clientRepository, createAuthorizationServer } from "./oauth-authorization-server"; @@ -58,10 +60,33 @@ export class OAuthController { const rt = req.query.redirect_uri?.toString(); if (!rt || !rt.startsWith("http://127.0.0.1:")) { log.error(`/oauth/authorize: invalid returnTo URL: "${rt}"`); + } + + const client = await clientRepository.getByIdentifier(clientID); + if (client) { + if (typeof req.query.redirect_uri !== "string") { + log.error(req.query.redirect_uri ? "Missing redirect URI" : "Invalid format of redirect URI"); + res.sendStatus(400); + return false; + } + + const normalizedRedirectUri = new URL(req.query.redirect_uri); + normalizedRedirectUri.search = ""; + + if (!client.redirectUris.some((u) => new URL(u).toString() === normalizedRedirectUri.toString())) { + log.error(`/oauth/authorize: invalid returnTo URL: "${req.query.redirect_uri}"`); + res.sendStatus(400); + return false; + } + } else { + log.error(`/oauth/authorize unknown client id: "${clientID}"`); res.sendStatus(400); return false; } - res.redirect(`${rt}/?approved=no`); + + const redirectUri = new URL(req.query.redirect_uri); + redirectUri.searchParams.append("approved", "no"); + res.redirect(redirectUri.toString()); return false; } else if (wasApproved == "yes") { const additionalData = (user.additionalData = user.additionalData || {}); @@ -133,9 +158,9 @@ export class OAuthController { // Return the HTTP redirect response const oauthResponse = await authorizationServer.completeAuthorizationRequest(authRequest); - return handleResponse(req, res, oauthResponse); + return handleExpressResponse(res, oauthResponse); } catch (e) { - handleError(e, res); + handleExpressError(e, res); } }); @@ -143,43 +168,13 @@ export class OAuthController { const response = new OAuthResponse(res); try { const oauthResponse = await authorizationServer.respondToAccessTokenRequest(req, response); - return handleResponse(req, res, oauthResponse); + return handleExpressResponse(res, oauthResponse); } catch (e) { - handleError(e, res); + handleExpressError(e, res); return; } }); - function handleError(e: Error | undefined, res: express.Response) { - if (e instanceof OAuthException) { - res.status(e.status); - res.send({ - status: e.status, - message: e.message, - stack: e.stack, - }); - return; - } - // Generic error - res.status(500); - res.send({ - err: e, - }); - } - - function handleResponse(req: express.Request, res: express.Response, response: OAuthResponse) { - if (response.status === 302) { - if (!response.headers.location) { - throw new Error("missing redirect location"); - } - res.set(response.headers); - res.redirect(response.headers.location); - } else { - res.set(response.headers); - res.status(response.status).send(response.body); - } - } - return router; } } diff --git a/yarn.lock b/yarn.lock index 05e1e24964a5c9..6e984daa40821f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1727,10 +1727,10 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@jmondi/oauth2-server@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@jmondi/oauth2-server/-/oauth2-server-1.1.1.tgz#cb2530c17e2c7db3cc632bb68ec0f38a54c7ae1c" - integrity sha512-my3776n6TDsJQJ+nONG52VNgTQ7veH9lo4kb/AAWt9Rko6VBuMxOb/KxcYdkDrpOznJ036+tVveuhY7zSJjGYg== +"@jmondi/oauth2-server@^2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@jmondi/oauth2-server/-/oauth2-server-2.2.2.tgz#e99b6edcd068c1a58423e2e8e94eeb0acb048b39" + integrity sha512-U9038EvDQJwc6SUxGjNfP1nhcyIzUuo4MLDBjWEp4ieuoyMYFBlMtmUbXcIEvvlwWQSXneUm7+TcTBcNHKrE8w== dependencies: jsonwebtoken "^8.5.1" ms "^2.1.3"