Skip to content

Commit

Permalink
[server] add vscode(-insiders) ouath2 clients
Browse files Browse the repository at this point in the history
  • Loading branch information
akosyakov committed Oct 22, 2021
1 parent a996c98 commit 1029829
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 63 deletions.
2 changes: 1 addition & 1 deletion components/gitpod-db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
],
"dependencies": {
"@gitpod/gitpod-protocol": "0.1.5",
"@jmondi/oauth2-server": "^1.1.0",
"@jmondi/oauth2-server": "^2.1.0",
"mysql": "^2.15.0",
"reflect-metadata": "^0.1.10",
"the-big-username-blacklist": "^1.5.2",
Expand Down
4 changes: 2 additions & 2 deletions components/gitpod-db/src/typeorm/auth-code-repository-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class AuthCodeRepositoryDB implements OAuthAuthCodeRepository {
return (await this.getEntityManager()).getRepository<DBOAuthAuthCodeEntry>(DBOAuthAuthCodeEntry);
}

public async getByIdentifier(authCodeCode: string): Promise<OAuthAuthCode> {
public async getByIdentifier(authCodeCode: string): Promise<DBOAuthAuthCodeEntry> {
const authCodeRepo = await this.getOauthAuthCodeRepo();
let authCodes = await authCodeRepo.find({ code: authCodeCode });
authCodes = authCodes.filter(te => (new Date(te.expiresAt)).getTime() > Date.now());
Expand All @@ -48,7 +48,7 @@ export class AuthCodeRepositoryDB implements OAuthAuthCodeRepository {
scopes: scopes,
};
}
public async persist(authCode: OAuthAuthCode): Promise<void> {
public async persist(authCode: DBOAuthAuthCodeEntry): Promise<void> {
const authCodeRepo = await this.getOauthAuthCodeRepo();
authCodeRepo.save(authCode);
}
Expand Down
4 changes: 2 additions & 2 deletions components/gitpod-db/src/typeorm/entity/db-oauth-auth-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -38,7 +38,7 @@ export class DBOAuthAuthCodeEntry implements OAuthAuthCode {
type: "varchar",
length: 10,
})
codeChallengeMethod: string
codeChallengeMethod: CodeChallengeMethod

@Column({
type: 'timestamp',
Expand Down
2 changes: 1 addition & 1 deletion components/gitpod-db/src/typeorm/user-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ export class TypeORMUserDBImpl implements UserDB {
} else {
var user: MaybeUser;
if (accessToken.user) {
user = await this.findUserById(accessToken.user.id)
user = await this.findUserById(String(accessToken.user.id))
}
dbToken = {
tokenHash,
Expand Down
2 changes: 1 addition & 1 deletion components/proxy/conf/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions components/proxy/plugins/corsorigin/cors_origin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand All @@ -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,
Expand Down Expand Up @@ -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":
Expand All @@ -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")
}

Expand Down
2 changes: 1 addition & 1 deletion components/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.1.0",
"@octokit/rest": "18.5.6",
"@probot/get-private-key": "1.1.0",
"amqplib": "^0.5.2",
Expand Down
31 changes: 27 additions & 4 deletions components/server/src/oauth-server/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface InMemory {
}

// Scopes
const scopes: OAuthScope[] = [
const localAppScopes: OAuthScope[] = [
{ name: "function:getGitpodTokenScopes" },
{ name: "function:getWorkspace" },
{ name: "function:getWorkspaces" },
Expand All @@ -35,16 +35,39 @@ const localClient: OAuthClient = {
// NOTE: these need to be kept in sync with the port range in the local app
redirectUris: Array.from({ length: 10 }, (_, i) => 'http://127.0.0.1:' + (63110 + i)),
allowedGrants: ['authorization_code'],
scopes,
scopes: localAppScopes,
}

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,
[vscode.id]: vscode,
[vscodeInsiders.id]: vscodeInsiders
},
tokens: {},
scopes: {},
};
for (const scope of scopes) {
inMemoryDatabase.scopes[scope.name] = scope;
for (const clientId in inMemoryDatabase.clients) {
const client = inMemoryDatabase.clients[clientId];
for (const scope of client.scopes) {
inMemoryDatabase.scopes[scope.name] = scope;
}
}
65 changes: 24 additions & 41 deletions components/server/src/oauth-server/oauth-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -40,7 +42,7 @@ export class OAuthController {
}

private async hasApproval(user: User, clientID: string, req: express.Request, res: express.Response): Promise<boolean> {
// Have they just authorized, or not, the local-app?
// Have they just authorized, or not, registered clients?
const wasApproved = req.query['approved'] || '';
if (wasApproved === 'no') {
const additionalData = user?.additionalData;
Expand All @@ -49,14 +51,25 @@ export class OAuthController {
await this.userDb.updateUserPartial(user);
}

// Let the local app know they rejected the approval
const rt = req.query.redirect_uri;
if (!rt || !rt.startsWith("http://127.0.0.1:")) {
log.error(`/oauth/authorize: invalid returnTo URL: "${rt}"`)
// Let the client know they rejected the approval
const client = await clientRepository.getByIdentifier(clientID);
if (client) {
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 || {};
Expand Down Expand Up @@ -123,53 +136,23 @@ 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);
}
});

router.post("/oauth/token", async (req: express.Request, res: express.Response) => {
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;
}
}
23 changes: 15 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3286,10 +3286,10 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"

"@jmondi/oauth2-server@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@jmondi/oauth2-server/-/oauth2-server-1.1.0.tgz#37014c4aceaee9b4559df224a4d3a743e66e5929"
integrity sha512-7UuliIJVnn3ISVEQ/CeRdKdY6gQb+RbOAlCZysdWytcvWNF+5Xb32ARbjOnzzLRzlh5ZxAKC5Zta0TZSKeXchg==
"@jmondi/oauth2-server@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@jmondi/oauth2-server/-/oauth2-server-2.1.0.tgz#ffa10dd8b9c5c8b480824bf0ecf104d5d00ec4a7"
integrity sha512-R6zxiKCC0MyAk3M9rV8gM0bDqvXNZgiDLTriefkfsZIXZoVw52W2X8usf5Y8qSwnmdP4u3ijXbb2fSUAcSbDdA==
dependencies:
jsonwebtoken "^8.5.1"
ms "^2.1.3"
Expand Down Expand Up @@ -4675,7 +4675,7 @@
"@types/history" "*"
"@types/react" "*"

"@types/react@*", "@types/react@^17.0.0":
"@types/react@*", "@types/react@17.0.0", "@types/react@^17.0.0":
version "17.0.0"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8"
integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==
Expand Down Expand Up @@ -14874,7 +14874,7 @@ mz@^2.4.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"

nan@^2.12.1, nan@^2.13.2, nan@^2.9.2:
nan@2.14.1, nan@^2.12.1, nan@^2.13.2, nan@^2.14.0, nan@^2.9.2:
version "2.14.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
Expand Down Expand Up @@ -15421,6 +15421,13 @@ onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"

oniguruma@7.2.1:
version "7.2.1"
resolved "https://registry.yarnpkg.com/oniguruma/-/oniguruma-7.2.1.tgz#51775834f7819b6e31aa878706aa7f65ad16b07f"
integrity sha512-WPS/e1uzhswPtJSe+Zls/kAj27+lEqZjCmRSjnYk/Z4L2Mu+lJC2JWtkZhPJe4kZeTQfz7ClcLyXlI4J68MG2w==
dependencies:
nan "^2.14.0"

open@^7.0.2:
version "7.4.2"
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
Expand Down Expand Up @@ -17555,7 +17562,7 @@ react-dev-utils@^11.0.3:
strip-ansi "6.0.0"
text-table "0.2.0"

react-dom@^17.0.1:
react-dom@17.0.1, react-dom@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==
Expand Down Expand Up @@ -17684,7 +17691,7 @@ react-scripts@^4.0.3:
optionalDependencies:
fsevents "^2.1.3"

react@^17.0.1:
react@17.0.1, react@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==
Expand Down

0 comments on commit 1029829

Please sign in to comment.