-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Prototype of login via prototyper in Studio #8938
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 7 commits
a3e984f
13bbdcf
23cfabd
381f8bb
1381a9f
d62df06
c7ff264
ef16f04
e807626
418129e
8150032
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 | ||||
---|---|---|---|---|---|---|
|
@@ -71,8 +71,8 @@ | |||||
return getGlobalDefaultAccount(); | ||||||
} | ||||||
|
||||||
const activeAccounts = configstore.get("activeAccounts") || {}; | ||||||
const email: string | undefined = activeAccounts[projectDir]; | ||||||
Check warning on line 75 in src/auth.ts
|
||||||
|
||||||
if (!email) { | ||||||
return getGlobalDefaultAccount(); | ||||||
|
@@ -86,7 +86,7 @@ | |||||
* Get all authenticated accounts _besides_ the default account. | ||||||
*/ | ||||||
export function getAdditionalAccounts(): Account[] { | ||||||
return configstore.get("additionalAccounts") || []; | ||||||
} | ||||||
|
||||||
/** | ||||||
|
@@ -108,7 +108,7 @@ | |||||
/** | ||||||
* Throw an error if the provided email is not a signed-in user. | ||||||
*/ | ||||||
export function assertAccount(email: string, options?: { mcp?: boolean }) { | ||||||
const allAccounts = getAllAccounts(); | ||||||
const accountExists = allAccounts.some((a) => a.user.email === email); | ||||||
if (!accountExists) { | ||||||
|
@@ -128,20 +128,20 @@ | |||||
* @param options options object. | ||||||
* @param account account to make active. | ||||||
*/ | ||||||
export function setActiveAccount(options: any, account: Account) { | ||||||
Check warning on line 131 in src/auth.ts
|
||||||
if (account.tokens.refresh_token) { | ||||||
setRefreshToken(account.tokens.refresh_token); | ||||||
} | ||||||
|
||||||
options.user = account.user; | ||||||
options.tokens = account.tokens; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Set the global refresh token in both api and apiv2. | ||||||
* @param token refresh token string | ||||||
*/ | ||||||
export function setRefreshToken(token: string) { | ||||||
apiv2.setRefreshToken(token); | ||||||
} | ||||||
|
||||||
|
@@ -425,6 +425,67 @@ | |||||
return base64string.replace(/\+/g, "-").replace(/=+$/, "").replace(/\//g, "_"); | ||||||
} | ||||||
|
||||||
interface PrototyperRes { | ||||||
uri: string; | ||||||
sessionId: string; | ||||||
authorize: (authorizationCode: string) => Promise<UserCredentials>; | ||||||
} | ||||||
|
||||||
export async function loginPrototyper(): Promise<PrototyperRes> { | ||||||
const authProxyClient = new apiv2.Client({ | ||||||
urlPrefix: authProxyOrigin(), | ||||||
auth: false, | ||||||
}); | ||||||
|
||||||
const sessionId = uuidv4(); | ||||||
const codeVerifier = randomBytes(32).toString("hex"); | ||||||
// urlsafe base64 is required for code_challenge in OAuth PKCE | ||||||
const codeChallenge = urlsafeBase64(createHash("sha256").update(codeVerifier).digest("base64")); | ||||||
|
||||||
const attestToken = ( | ||||||
await authProxyClient.post<{ session_id: string }, { token: string }>("/attest", { | ||||||
session_id: sessionId, | ||||||
}) | ||||||
).body?.token; | ||||||
maneesht marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
const loginUrl = `${authProxyOrigin()}/login?code_challenge=${codeChallenge}&session=${sessionId}&attest=${attestToken}&studio_prototyper=true}`; | ||||||
return { | ||||||
uri: loginUrl, | ||||||
sessionId: sessionId.substring(0, 5).toUpperCase(), | ||||||
authorize: async (code: string) => { | ||||||
try { | ||||||
const tokens = await getTokensFromAuthorizationCode( | ||||||
code, | ||||||
`${authProxyOrigin()}/complete`, | ||||||
codeVerifier, | ||||||
); | ||||||
|
||||||
const creds = { | ||||||
user: jwt.decode(tokens.id_token!, { json: true }) as any as User, | ||||||
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. You should be able to use the generic type:
Suggested change
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. JWT doesn't have a generic typed version of decode 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. Man, that's what I get for not researching an LLM response :( |
||||||
tokens: tokens, | ||||||
scopes: SCOPES, | ||||||
}; | ||||||
recordCredentials(creds); | ||||||
return creds; | ||||||
} catch (e) { | ||||||
throw new FirebaseError( | ||||||
"Unable to authenticate using the provided code. Please try again.", | ||||||
); | ||||||
} | ||||||
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. The } catch (e: unknown) {
logger.debug("Prototyper login authorization failed:", e);
throw new FirebaseError(
"Unable to authenticate using the provided code. Please try again.",
);
} 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. Agree with the above 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. Good catch - this can just throw the original error tbh, no need to clean it up for the module consumer. |
||||||
}, | ||||||
}; | ||||||
} | ||||||
|
||||||
// recordCredentials saves credentials to configstore to be used in future command runs. | ||||||
export function recordCredentials(creds: UserCredentials) { | ||||||
configstore.set("user", creds.user); | ||||||
configstore.set("tokens", creds.tokens); | ||||||
// store login scopes in case mandatory scopes grow over time | ||||||
configstore.set("loginScopes", creds.scopes); | ||||||
// remove old session token, if it exists | ||||||
configstore.delete("session"); | ||||||
} | ||||||
|
||||||
async function loginRemotely(): Promise<UserCredentials> { | ||||||
const authProxyClient = new apiv2.Client({ | ||||||
urlPrefix: authProxyOrigin(), | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we call this multiple times, is this something we can just cache once and re-use?