-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Initial version of OAuth server to manage access to Gitpod workspaces #4222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/** | ||
* Copyright (c) 2021 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 gitpodIcon from './icons/gitpod.svg'; | ||
import { getSafeURLRedirect } from "./provider-utils"; | ||
|
||
export default function OAuthClientApproval() { | ||
const params = new URLSearchParams(window.location.search); | ||
const clientName = params.get("clientName") || ""; | ||
let redirectToParam = params.get("redirectTo") || undefined; | ||
if (redirectToParam) { | ||
redirectToParam = decodeURIComponent(redirectToParam); | ||
} | ||
const redirectTo = getSafeURLRedirect(redirectToParam) || "/"; | ||
const updateClientApproval = async (isApproved: boolean) => { | ||
if (redirectTo === "/") { | ||
window.location.replace(redirectTo); | ||
} | ||
window.location.replace(`${redirectTo}&approved=${isApproved ? 'yes' : 'no'}`); | ||
} | ||
|
||
return (<div id="oauth-container" className="z-50 flex w-screen h-screen"> | ||
<div id="oauth-section" className="flex-grow flex w-full"> | ||
<div id="oauth-section-column" className="flex-grow max-w-2xl flex flex-col h-100 mx-auto"> | ||
<div className="flex-grow h-100 flex flex-row items-center justify-center" > | ||
<div className="rounded-xl px-10 py-10 mx-auto"> | ||
<div className="mx-auto pb-8"> | ||
<img src={gitpodIcon} className="h-16 mx-auto" /> | ||
</div> | ||
<div className="mx-auto text-center pb-8 space-y-2"> | ||
<h1 className="text-3xl">Authorize {clientName}</h1> | ||
<h4>You are about to authorize {clientName} to access your Gitpod account including data for all workspaces.</h4> | ||
</div> | ||
<div className="flex justify-center mt-6"> | ||
<button className="secondary" onClick={() => updateClientApproval(false)}>Cancel</button> | ||
<button key={"button-yes"} className="ml-2" onClick={() => updateClientApproval(true)}> | ||
Authorize | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div>); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
## Gitpod-db | ||
|
||
Contains all the database related functionality, implemented using [typeorm](https://typeorm.io/). | ||
|
||
### Adding a new table | ||
1. Create a [migration](./src/typeorm/migration/README.md) - use the [baseline](./src/typeorm/migration/1592203031938-Baseline.ts) as an exemplar | ||
1. Create a new entity that implements the requisite interface or extend an existing entity as required - see [db-user.ts](./src/typeorm/entity/db-user.ts) | ||
1. If it is a new table, create the matching injectable ORM implementation and interface (if required) - see [user-db-impl.ts](./src/typeorm/user-db-impl.ts) and [user-db.ts](./src/user-db.ts). Otherwise extend the existing interface and implementation as required. | ||
1. Add the injectable implementation to the [DB container module](./src/container-module.ts), binding the interface and implementation as appropriate, otherwise it will not be instantiated correctly e.g. | ||
``` | ||
bind(TypeORMUserDBImpl).toSelf().inSingletonScope(); | ||
bind(UserDB).toService(TypeORMUserDBImpl); | ||
``` | ||
1. Add the new ORM as an injected component where required e.g. in [user-controller.ts](./src/user/user-controller.ts) | ||
``` | ||
@inject(UserDB) protected readonly userDb: UserDB; | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/** | ||
* Copyright (c) 2021 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 { DateInterval, OAuthAuthCode, OAuthAuthCodeRepository, OAuthClient, OAuthScope, OAuthUser } from "@jmondi/oauth2-server"; | ||
import * as crypto from 'crypto'; | ||
import { inject, injectable } from "inversify"; | ||
import { EntityManager, Repository } from "typeorm"; | ||
import { DBOAuthAuthCodeEntry } from './entity/db-oauth-auth-code'; | ||
import { TypeORM } from './typeorm'; | ||
|
||
const expiryInFuture = new DateInterval("5m"); | ||
|
||
@injectable() | ||
export class AuthCodeRepositoryDB implements OAuthAuthCodeRepository { | ||
|
||
@inject(TypeORM) | ||
private readonly typeORM: TypeORM; | ||
|
||
protected async getEntityManager(): Promise<EntityManager> { | ||
return (await this.typeORM.getConnection()).manager; | ||
} | ||
|
||
async getOauthAuthCodeRepo(): Promise<Repository<DBOAuthAuthCodeEntry>> { | ||
return (await this.getEntityManager()).getRepository<DBOAuthAuthCodeEntry>(DBOAuthAuthCodeEntry); | ||
} | ||
|
||
public async getByIdentifier(authCodeCode: string): Promise<OAuthAuthCode> { | ||
const authCodeRepo = await this.getOauthAuthCodeRepo(); | ||
let authCodes = await authCodeRepo.find({ code: authCodeCode }); | ||
authCodes = authCodes.filter(te => (new Date(te.expiresAt)).getTime() > Date.now()); | ||
const authCode = authCodes.length > 0 ? authCodes[0] : undefined; | ||
if (!authCode) { | ||
throw new Error(`authentication code not found`); | ||
} | ||
return authCode; | ||
} | ||
public issueAuthCode(client: OAuthClient, user: OAuthUser | undefined, scopes: OAuthScope[]): OAuthAuthCode { | ||
const code = crypto.randomBytes(30).toString('hex'); | ||
// NOTE: caller (@jmondi/oauth2-server) is responsible for adding the remaining items, PKCE params, redirect URL, etc | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given, that this is called by the oauth2 server framework, I'd suggest to extract and move it to the server component. it seems odd to implement the missing bits for the framework within There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That requires moving the entire AuthCodeRepositoryDB (OAuthAuthCodeRepository) implementation to server and since it requires storage, then adding a db service + implementation for it to use. I agree it meets the current, very structured implementation approach for most parts of the system that interact with the db and will take a look at it tomorrow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
no it doesn't. this pattern is used in may cases here. the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not so much a missing bit but the OAuthAuthCodeRepository interface needs to be implemented for the framework to use. Moving 1 requires moving the rest (yes - there are other, less readable ways to do so).
Agreed. That's what I was referring to with "then adding a db service + implementation for it to use" |
||
return { | ||
code: code, | ||
user, | ||
client, | ||
expiresAt: expiryInFuture.getEndDate(), | ||
scopes: scopes, | ||
}; | ||
} | ||
public async persist(authCode: OAuthAuthCode): Promise<void> { | ||
const authCodeRepo = await this.getOauthAuthCodeRepo(); | ||
authCodeRepo.save(authCode); | ||
} | ||
public async isRevoked(authCodeCode: string): Promise<boolean> { | ||
const authCode = await this.getByIdentifier(authCodeCode); | ||
return Date.now() > authCode.expiresAt.getTime(); | ||
} | ||
public async revoke(authCodeCode: string): Promise<void> { | ||
const authCode = await this.getByIdentifier(authCodeCode); | ||
if (authCode) { | ||
// Set date to earliest timestamp that MySQL allows | ||
authCode.expiresAt = new Date(1000); | ||
return this.persist(authCode); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.