Skip to content

Commit

Permalink
[server] add Unauthenticated decorator for public-api
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexTugarev committed Nov 8, 2023
1 parent e70205e commit bc390a6
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 8 deletions.
31 changes: 23 additions & 8 deletions components/server/src/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { APITeamsService as TeamsServiceAPI } from "./teams";
import { APIUserService as UserServiceAPI } from "./user";
import { WorkspaceServiceAPI } from "./workspace-service-api";
import { AuthProviderServiceAPI } from "./auth-provider-service-api";
import { Unauthenticated } from "./unauthenticated";

decorate(injectable(), PublicAPIConverter);

Expand Down Expand Up @@ -214,9 +215,21 @@ export class API {

const apply = async <T>(): Promise<T> => {
const subjectId = await self.verify(context);
await rateLimit(subjectId);
context.user = await self.ensureFgaMigration(subjectId);
const isAuthenticated = !!subjectId;
const requiresAuthentication = !Unauthenticated.get(target, prop);

if (!isAuthenticated && requiresAuthentication) {
throw new ConnectError("unauthenticated", Code.Unauthenticated);
}

if (isAuthenticated) {
await rateLimit(subjectId);
context.user = await self.ensureFgaMigration(subjectId);
}

// TODO(at) if unauthenticated, we still need to apply enforece a rate limit

// actually call the RPC handler
return Reflect.apply(target[prop as any], target, args);
};
if (grpc_type === "unary" || grpc_type === "client_stream") {
Expand Down Expand Up @@ -250,14 +263,16 @@ export class API {
};
}

private async verify(context: HandlerContext): Promise<string> {
private async verify(context: HandlerContext): Promise<string | undefined> {
const cookieHeader = (context.requestHeader.get("cookie") || "") as string;
const claims = await this.sessionHandler.verifyJWTCookie(cookieHeader);
const subjectId = claims?.sub;
if (!subjectId) {
throw new ConnectError("unauthenticated", Code.Unauthenticated);
try {
const claims = await this.sessionHandler.verifyJWTCookie(cookieHeader);
const subjectId = claims?.sub;
return subjectId;
} catch (error) {
log.warn("Failed to authenticate user with JWT Session", error);
return undefined;
}
return subjectId;
}

private async ensureFgaMigration(userId: string): Promise<User> {
Expand Down
28 changes: 28 additions & 0 deletions components/server/src/api/unauthenticated.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2023 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 * as chai from "chai";
import { Unauthenticated } from "./unauthenticated";

const expect = chai.expect;

class Foo {
@Unauthenticated()
async fooUnauthenticated() {}

async foo() {}
}

describe("Unauthenticated decorator", function () {
const foo = new Foo();

it("function is decorated", function () {
expect(Unauthenticated.get(foo, "fooUnauthenticated")).to.be.true;
});
it("function is not decorated", function () {
expect(Unauthenticated.get(foo, "foo")).to.be.false;
});
});
17 changes: 17 additions & 0 deletions components/server/src/api/unauthenticated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2023 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.
*/

const UNAUTHENTICATED_METADATA_KEY = Symbol("Unauthenticated");

export function Unauthenticated() {
return Reflect.metadata(UNAUTHENTICATED_METADATA_KEY, true);
}

export namespace Unauthenticated {
export function get(target: Object, properyKey: string | symbol): boolean {
return !!Reflect.getMetadata(UNAUTHENTICATED_METADATA_KEY, target, properyKey);
}
}

0 comments on commit bc390a6

Please sign in to comment.