gitpod/components/server/src/oauth-server/oauth-controller.ts
2022-09-05 19:27:20 +02:00

193 lines
7.8 KiB
TypeScript

/**
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/
import { AuthCodeRepositoryDB } from "@gitpod/gitpod-db/lib/typeorm/auth-code-repository-db";
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 { 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";
import { inMemoryDatabase } from "./db";
@injectable()
export class OAuthController {
@inject(Config) protected readonly config: Config;
@inject(UserDB) protected readonly userDb: UserDB;
@inject(AuthCodeRepositoryDB) protected readonly authCodeRepositoryDb: AuthCodeRepositoryDB;
private getValidUser(req: express.Request, res: express.Response): User | null {
if (!req.isAuthenticated() || !User.is(req.user)) {
const redirectTarget = encodeURIComponent(`${this.config.hostUrl}api${req.originalUrl}`);
const redirectTo = `${this.config.hostUrl}login?returnTo=${redirectTarget}`;
res.redirect(redirectTo);
return null;
}
const user = req.user as User;
if (!user) {
res.sendStatus(500);
return null;
}
if (user.blocked) {
res.sendStatus(403);
return null;
}
return user;
}
private async hasApproval(
user: User,
clientID: string,
req: express.Request,
res: express.Response,
): Promise<boolean> {
// Have they just authorized, or not, the local-app?
const wasApproved = req.query["approved"] || "";
if (wasApproved === "no") {
const additionalData = user?.additionalData;
if (additionalData && additionalData.oauthClientsApproved) {
delete additionalData.oauthClientsApproved[clientID];
await this.userDb.updateUserPartial(user);
}
// Let the local app know they rejected the approval
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;
}
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 || {});
additionalData.oauthClientsApproved = {
...additionalData.oauthClientsApproved,
[clientID]: new Date().toISOString(),
};
await this.userDb.updateUserPartial(user);
} else {
const oauthClientsApproved = user?.additionalData?.oauthClientsApproved;
if (!oauthClientsApproved || !oauthClientsApproved[clientID]) {
const client = await clientRepository.getByIdentifier(clientID);
if (client) {
const redirectTarget = encodeURIComponent(`${this.config.hostUrl}api${req.originalUrl}`);
const redirectTo = `${this.config.hostUrl}oauth-approval?clientID=${client.id}&clientName=${client.name}&returnTo=${redirectTarget}`;
res.redirect(redirectTo);
return false;
} else {
log.error(`/oauth/authorize unknown client id: "${clientID}"`);
res.sendStatus(400);
return false;
}
}
}
return true;
}
get oauthRouter(): express.Router {
const router = express.Router();
if (!this.config.oauthServer.enabled) {
log.warn("OAuth server disabled!");
return router;
}
const authorizationServer = createAuthorizationServer(
this.authCodeRepositoryDb,
this.userDb,
this.userDb,
this.config.oauthServer.jwtSecret,
);
router.get("/oauth/authorize", async (req: express.Request, res: express.Response) => {
const clientID = req.query.client_id;
if (!clientID) {
res.sendStatus(400);
return false;
}
const user = this.getValidUser(req, res);
if (!user) {
return;
}
// Check for approval of this client
if (!this.hasApproval(user, clientID.toString(), req, res)) {
return;
}
const request = new OAuthRequest(req);
try {
// Validate the HTTP request and return an AuthorizationRequest object.
const authRequest = await authorizationServer.validateAuthorizationRequest(request);
// Once the user has logged in set the user on the AuthorizationRequest
authRequest.user = { id: user.id };
// The user has approved the client so update the status
authRequest.isAuthorizationApproved = true;
// Return the HTTP redirect response
const oauthResponse = await authorizationServer.completeAuthorizationRequest(authRequest);
return handleExpressResponse(res, oauthResponse);
} catch (e) {
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 handleExpressResponse(res, oauthResponse);
} catch (e) {
handleExpressError(e, res);
return;
}
});
router.get("/oauth/inspect", async (req: express.Request, res: express.Response) => {
const clientId = req.query.client as string;
if (typeof clientId !== "string" || !Object.keys(inMemoryDatabase.clients).includes(clientId)) {
return res.sendStatus(400);
}
const client = inMemoryDatabase.clients[clientId];
const scopes = client.scopes.map((s) => s.name);
return res.send(scopes);
});
return router;
}
}