Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
327 changes: 194 additions & 133 deletions src/auth/AuthProvider/index.ts

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions src/auth/AuthProvider/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { AuthenticationSession, EventEmitter, Uri } from "vscode";
import { PromiseAdapter } from "./utils/promiseFromEvent";

export type User = {
id: number;
userName: string;
Expand Down Expand Up @@ -28,3 +31,37 @@ export type UserInfo = {
needConsent: boolean;
defaultWorkspaceId: number;
};

export type WebCallback = {
promise: Promise<string>;
cancel: EventEmitter<void>;
};

export type AuthSession = AuthenticationSession & {
refreshToken?: string;
};

export type ResponseAuth0 = {
access_token: string;
refresh_token?: string;
token_type: "Bearer";
expires_in: number;
scope: string;
id_token: string;
};

export type UserInfoAuth0 = {
email: string;
email_verified: boolean;
family_name: string;
given_name: string;
name: string;
nickname: string;
picture: string;
preferred_username: string;
sub: string;
updated_at: string;
};

export type Auth0LoginType = "code" | "token";
export type WebCallbackHandler = PromiseAdapter<Uri, string>;
27 changes: 22 additions & 5 deletions src/auth/getAccessToken.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import { AuthenticationSession, ExtensionContext } from "vscode";
import { ExtensionContext } from "vscode";
import { jwtExpired } from "./AuthProvider/utils/jwt";

import { STORAGE_KEY_NAME } from "./AuthProvider";
import { STORAGE_KEY } from "./AuthProvider";
import refreshAccessToken from "./refreshAccessToken";
import { AuthSession } from "./AuthProvider/types";

export type Auth0TokenResponse = {
access_token: string;
refresh_token?: string;
expires_in: number;
token_type: string;
scope: string;
};

const getAccessToken = async (
context: ExtensionContext
): Promise<string | undefined> => {
const sessionsStr = await context.secrets.get(STORAGE_KEY_NAME);
const sessionsStr = await context.secrets.get(STORAGE_KEY);
const sessions = sessionsStr ? JSON.parse(sessionsStr) : [];
const session = sessions[0] as AuthenticationSession;
const token = session?.accessToken;
const session = sessions[0] as AuthSession;
let token = session?.accessToken;

// Check if token is expired
if (token && jwtExpired(token)) {
const newToken = await refreshAccessToken(session, context);
if (newToken) return newToken;
}
return token;
};

Expand Down
52 changes: 52 additions & 0 deletions src/auth/refreshAccessToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// import fetch from "node-fetch";

import { STORAGE_KEY } from "./AuthProvider";
import {
AUTH0_CLIENT_ID,
AUTH0_CLIENT_SECRET,
AUTH0_DOMAIN
} from "../constants";
import { Auth0TokenResponse } from "./getAccessToken";
import { ExtensionContext } from "vscode";
import { AuthSession } from "./AuthProvider/types";

const refreshAccessToken = async (
session: AuthSession,
context: ExtensionContext
): Promise<string | undefined> => {
const refreshToken = session.refreshToken;
if (!refreshToken) return undefined;

try {
const data = new URLSearchParams([
["grant_type", "refresh_token"],
["client_id", AUTH0_CLIENT_ID],
["client_secret", AUTH0_CLIENT_SECRET],
["refresh_token", refreshToken]
]);

const response = await fetch(`${AUTH0_DOMAIN}/oauth/token`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: data.toString()
});

const tokens = (await response.json()) as Auth0TokenResponse;
const { access_token, refresh_token } = tokens;
if (!access_token) throw new Error(`No new access token`);

const updatedSession: AuthSession = {
...session,
accessToken: access_token,
refreshToken: refresh_token || session.refreshToken
};
await context.secrets.store(STORAGE_KEY, JSON.stringify([updatedSession]));
return access_token;
} catch (err) {
console.log("🔴 Failed to refresh token", err);
return undefined;
}
return undefined;
};

export default refreshAccessToken;
12 changes: 11 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
export const SEQERA_PLATFORM_URL = `https://cloud.seqera.io`;
// TODO: Restore to cloud.seqera.io
export const SEQERA_PLATFORM_URL = `https://pr-8246.dev-tower.net`;
export const SEQERA_API_URL = `${SEQERA_PLATFORM_URL}/api`;
export const SEQERA_HUB_API_URL = `https://hub.seqera.io`;
export const SEQERA_INTERN_API_URL = `https://intern.seqera.io`;

// TODO: Use env var to store the Auth0 secret
// TODO: Update to production Auth0 app
// TODO: Security implications of rolling up this secret into the built extension
export const AUTH0_CLIENT_SECRET =
"tZ3N8vHuvpLQlzdGEhel4Vz5DeluNNyTtid-2jFBdDiXmIGNbX9yhjDmQ2Pg6VT-";
Comment on lines +9 to +11

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts:

  • It should be okay as long as we bear in mind that the client is considered a public client (contrary to a confidential client, which is capable of holding configuration time secrets), so the client should not be shared for purposes other than the VSCode plugin: the client is still bound by other measures (authentication by user-agent to authorization server, redirect URIs, ...).
  • Given that it is public, the client might as well have no authentication because the secret serves no real purpose at the end of the day (configurable through Auth0 dashboard).
  • For increased security in these cases, I've heard of PKCE , which might be worth looking into.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Providing a client secret is not needed when the app is defined as public. Can you try to make it work without this parameter?
As @tcrespog said, PKCE is used to mitigate the risks of now being able to manage a secret key. It is enabled by default.

export const AUTH0_CLIENT_ID = "7PJnvIXiXK3HkQR43c4zBf3bWuxISp9W";
export const AUTH0_SCOPES = "openid profile email offline_access";
export const AUTH0_DOMAIN = `seqera-development.eu.auth0.com`;
6 changes: 2 additions & 4 deletions src/webview/WebviewProvider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ class WebviewProvider implements vscode.WebviewViewProvider {

public async initViewData(refresh?: boolean) {
const { viewID, _context, _currentView: view } = this;
console.log("🟠 initViewData", viewID);
if (!view) return;
if (viewID === "seqeraCloud") {
this.getRepoInfo();
Expand Down Expand Up @@ -211,7 +210,7 @@ class WebviewProvider implements vscode.WebviewViewProvider {
const created = await createTest(filePath, accessToken);
this.emitTestCreated(filePath, created);
} catch (error) {
console.log("🟠 Test creation failed", error);
console.log("🔴 Test creation failed", error);
this.emitTestCreated(filePath, false);
}
}
Expand All @@ -227,12 +226,11 @@ class WebviewProvider implements vscode.WebviewViewProvider {

private async getContainer(filePath: string) {
const accessToken = await getAccessToken(this._context);

try {
const created = await getContainer(filePath, accessToken);
this.emitContainerCreated(filePath, created);
} catch (error) {
console.log("🟠 Container creation failed", error);
console.log("🔴 Container creation failed", error);
this.emitContainerCreated(filePath, false);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/webview/WebviewProvider/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as fetchPlatformData } from "./platform/fetchPlatformData";
export { default as clearPlatformData } from "./platform/clearPlatformData";
export { default as getAuthState } from "./platform/getAuthState";
export * from "./platform/utils";

Expand Down
12 changes: 12 additions & 0 deletions src/webview/WebviewProvider/lib/platform/clearPlatformData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ExtensionContext, WebviewView } from "vscode";

const clearPlatformData = async (
view: WebviewView["webview"] | undefined,
context: ExtensionContext
) => {
view?.postMessage({ authState: {} });
const vsCodeState = context.workspaceState;
vsCodeState.update("platformData", {});
};

export default clearPlatformData;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const fetchUserInfo = async (token: string): Promise<UserInfoResponse> => {
Authorization: `Bearer ${token}`
}
});
console.log("🟣 fetchUserInfo", response.status);
console.log("🟣 fetchUserInfo", response);
if (response.status === 401) {
throw new Error("Unauthorized");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useTowerContext } from "../../../Context";
import Select from "../../../components/Select";
import { getWorkspaceURL } from "../utils";
import Button from "../../../components/Button";
import { SEQERA_PLATFORM_URL } from "../../../../../src/constants";

const WorkspaceSelector = () => {
const {
Expand Down Expand Up @@ -36,7 +37,9 @@ const WorkspaceSelector = () => {
subtle
/>
) : (
<div>No workspaces found</div>
<Button subtle href={SEQERA_PLATFORM_URL} fullWidth>
No workspaces found
</Button>
)}
{!!manageURL && (
<Button
Expand Down