Skip to content

Refresh token functionality #132

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

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
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-";
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 @@ -153,7 +153,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 @@ -183,7 +182,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 @@ -199,12 +198,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