mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
CLI Login for NodeJS (#602)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Docker Emulator Test / docker (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Test / docker (push) Has been cancelled
Runs E2E API Tests / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Preview Docs / run (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Docker Emulator Test / docker (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Test / docker (push) Has been cancelled
Runs E2E API Tests / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Preview Docs / run (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
<!-- ELLIPSIS_HIDDEN -->
> [!IMPORTANT]
> Adds CLI authentication for NodeJS with new error handling and updates
dependencies.
>
> - **CLI Authentication**:
> - Adds `promptCliLogin()` method in `_StackClientAppImplIncomplete` to
handle CLI authentication, returning a refresh token or error.
> - Handles errors `CliAuthError`, `CliAuthExpiredError`, and
`CliAuthUsedError`.
> - **Error Handling**:
> - Adds `CliAuthError`, `CliAuthExpiredError`, and `CliAuthUsedError`
to `known-errors.tsx`.
> - **Dependencies**:
> - Adds `open` to `dependencies` in `package.json`.
>
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 98b0ccd0d2. It will automatically
update as commits are pushed.</sup>
<!-- ELLIPSIS_HIDDEN -->
---------
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
parent
7f74b082ff
commit
240f866900
@ -1209,6 +1209,36 @@ const InvalidPollingCodeError = createKnownErrorConstructor(
|
||||
(json: any) => [json] as const,
|
||||
);
|
||||
|
||||
const CliAuthError = createKnownErrorConstructor(
|
||||
KnownError,
|
||||
"CLI_AUTH_ERROR",
|
||||
(message: string) => [
|
||||
400,
|
||||
message,
|
||||
] as const,
|
||||
(json: any) => [json.message] as const,
|
||||
);
|
||||
|
||||
const CliAuthExpiredError = createKnownErrorConstructor(
|
||||
KnownError,
|
||||
"CLI_AUTH_EXPIRED_ERROR",
|
||||
(message: string = "CLI authentication request expired. Please try again.") => [
|
||||
400,
|
||||
message,
|
||||
] as const,
|
||||
(json: any) => [json.message] as const,
|
||||
);
|
||||
|
||||
const CliAuthUsedError = createKnownErrorConstructor(
|
||||
KnownError,
|
||||
"CLI_AUTH_USED_ERROR",
|
||||
(message: string = "This authentication token has already been used.") => [
|
||||
400,
|
||||
message,
|
||||
] as const,
|
||||
(json: any) => [json.message] as const,
|
||||
);
|
||||
|
||||
|
||||
const ApiKeyNotValid = createKnownErrorConstructor(
|
||||
KnownError,
|
||||
@ -1293,6 +1323,9 @@ export const KnownErrors = {
|
||||
AllOverloadsFailed,
|
||||
ProjectAuthenticationError,
|
||||
PermissionIdAlreadyExists,
|
||||
CliAuthError,
|
||||
CliAuthExpiredError,
|
||||
CliAuthUsedError,
|
||||
InvalidProjectAuthentication,
|
||||
ProjectKeyWithoutAccessType,
|
||||
InvalidAccessType,
|
||||
|
||||
@ -1576,6 +1576,102 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a CLI authentication process that allows a command line application
|
||||
* to get a refresh token for a user's account.
|
||||
*
|
||||
* This process works as follows:
|
||||
* 1. The CLI app calls this method, which initiates the auth process with the server
|
||||
* 2. The server returns a polling code and a login code
|
||||
* 3. The CLI app opens a browser window to the appUrl with the login code as a parameter
|
||||
* 4. The user logs in through the browser and confirms the authorization
|
||||
* 5. The CLI app polls for the refresh token using the polling code
|
||||
*
|
||||
* @param options Options for the CLI login
|
||||
* @param options.appUrl The URL of the app that will handle the CLI auth confirmation
|
||||
* @param options.expiresInMillis Optional duration in milliseconds before the auth attempt expires (default: 2 hours)
|
||||
* @param options.maxAttempts Optional maximum number of polling attempts (default: Infinity)
|
||||
* @param options.waitTimeMillis Optional time to wait between polling attempts (default: 2 seconds)
|
||||
* @param options.promptLink Optional function to call with the login URL to prompt the user to open the browser
|
||||
* @returns Result containing either the refresh token or an error
|
||||
*/
|
||||
async promptCliLogin(options: {
|
||||
appUrl: string,
|
||||
expiresInMillis?: number,
|
||||
maxAttempts?: number,
|
||||
waitTimeMillis?: number,
|
||||
promptLink?: (url: string) => void,
|
||||
}): Promise<Result<string, KnownErrors["CliAuthError"] | KnownErrors["CliAuthExpiredError"] | KnownErrors["CliAuthUsedError"]>> {
|
||||
// Step 1: Initiate the CLI auth process
|
||||
const response = await this._interface.sendClientRequest(
|
||||
"/auth/cli",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
expires_in_millis: options.expiresInMillis,
|
||||
}),
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return Result.error(new KnownErrors.CliAuthError(`Failed to initiate CLI auth: ${response.status} ${await response.text()}`));
|
||||
}
|
||||
|
||||
const initResult = await response.json();
|
||||
const pollingCode = initResult.polling_code;
|
||||
const loginCode = initResult.login_code;
|
||||
|
||||
// Step 2: Open the browser for the user to authenticate
|
||||
const url = `${options.appUrl}/handler/cli-auth-confirm?login_code=${encodeURIComponent(loginCode)}`;
|
||||
if (options.promptLink) {
|
||||
options.promptLink(url);
|
||||
} else {
|
||||
console.log(`Please visit the following URL to authenticate:\n${url}`);
|
||||
}
|
||||
|
||||
|
||||
// Step 3: Poll for the token
|
||||
let attempts = 0;
|
||||
while (attempts < (options.maxAttempts ?? Infinity)) {
|
||||
attempts++;
|
||||
const pollResponse = await this._interface.sendClientRequest("/auth/cli/poll", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
polling_code: pollingCode,
|
||||
}),
|
||||
}, null);
|
||||
|
||||
if (!pollResponse.ok) {
|
||||
return Result.error(new KnownErrors.CliAuthError(`Failed to initiate CLI auth: ${pollResponse.status} ${await pollResponse.text()}`));
|
||||
}
|
||||
const pollResult = await pollResponse.json();
|
||||
|
||||
if (pollResponse.status === 201 && pollResult.status === "success") {
|
||||
return Result.ok(pollResult.refresh_token);
|
||||
}
|
||||
if (pollResult.status === "waiting") {
|
||||
await wait(options.waitTimeMillis ?? 2000);
|
||||
continue;
|
||||
}
|
||||
if (pollResult.status === "expired") {
|
||||
return Result.error(new KnownErrors.CliAuthExpiredError("CLI authentication request expired. Please try again."));
|
||||
}
|
||||
if (pollResult.status === "used") {
|
||||
return Result.error(new KnownErrors.CliAuthUsedError("This authentication token has already been used."));
|
||||
}
|
||||
return Result.error(new KnownErrors.CliAuthError(`Unexpected status from CLI auth polling: ${pollResult.status}`));
|
||||
}
|
||||
|
||||
return Result.error(new KnownErrors.CliAuthError("Timed out waiting for CLI authentication."));
|
||||
}
|
||||
|
||||
async signInWithPasskey(): Promise<Result<undefined, KnownErrors["PasskeyAuthenticationFailed"] | KnownErrors["InvalidTotpCode"] | KnownErrors["PasskeyWebAuthnError"]>> {
|
||||
this._ensurePersistentTokenStore();
|
||||
const session = await this._getSession();
|
||||
|
||||
@ -42,6 +42,7 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
|
||||
signUpWithCredential(options: { email: string, password: string, noRedirect?: boolean, verificationCallbackUrl?: string }): Promise<Result<undefined, KnownErrors["UserWithEmailAlreadyExists"] | KnownErrors["PasswordRequirementsNotMet"]>>,
|
||||
signInWithPasskey(): Promise<Result<undefined, KnownErrors["PasskeyAuthenticationFailed"]| KnownErrors["InvalidTotpCode"] | KnownErrors["PasskeyWebAuthnError"]>>,
|
||||
callOAuthCallback(): Promise<boolean>,
|
||||
promptCliLogin(options: { appUrl: string, expiresInMillis?: number }): Promise<Result<string, KnownErrors["CliAuthError"] | KnownErrors["CliAuthExpiredError"] | KnownErrors["CliAuthUsedError"]>>,
|
||||
sendForgotPasswordEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<undefined, KnownErrors["UserNotFound"]>>,
|
||||
sendMagicLinkEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<{ nonce: string }, KnownErrors["RedirectUrlNotWhitelisted"]>>,
|
||||
resetPassword(options: { code: string, password: string }): Promise<Result<undefined, KnownErrors["VerificationCodeError"]>>,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user