@proteles/vue

Vue 3 composables and components for Proteles authentication, plus Nuxt/h3 server handlers. Tokens never reach the browser. Needs a Proteles account: sign up for free at proteles.com.
465
Install
npm install @proteles/vue
Latest Version:0.4.0
Vue:3
License:MIT
Last Updated:Oct 22, 2026
Links: GitHub  ·  npm
Maintainer: aaron_

@proteles/vue

You need a Proteles account. Proteles is a hosted sign-in service, and this package is a client for it: without an account it does nothing. Sign up for free on proteles.com, then register your app to get its issuer, client ID and secret.

Vue 3 composables and components for Proteles authentication, plus the Nuxt/h3 server handlers.

Tokens live only in an encrypted, httpOnly cookie set by your own server — the browser never receives one, only the sanitized user from /api/auth/me.

Install

npm install @proteles/vue

The five-minute setup (Nuxt)

1. The auth routes — the whole OAuth flow, in one file:

// server/routes/api/auth/[...proteles].ts
import { protelesAuthHandler } from "@proteles/vue/server";

export default defineEventHandler((event) => protelesAuthHandler(event));

2. Server middleware — put the user on the event and guard routes:

// server/middleware/proteles.ts
import { getUser, guardRequest, CONTEXT_USER_KEY } from "@proteles/vue/server";

export default defineEventHandler(async (event) => {
  event.context[CONTEXT_USER_KEY] = await getUser(event);
  const redirect = await guardRequest(event, { publicPaths: ["/"] });
  if (redirect) return redirect;
});

3. Install the plugin with the server-known user, so the first paint is right. Take the user the middleware put on event.context — not useFetch("/api/auth/me") during server rendering, which refreshes an expired access token and rotates the refresh token without Nuxt passing the new cookie on to the browser, which is then signed out on its next refresh:

// plugins/proteles.ts (as in the Nuxt quickstart)
import { protelesAuth } from "@proteles/vue";

type InitialUser = { user: Record<string, unknown> | null; known: boolean };

export default defineNuxtPlugin(async (nuxtApp) => {
  // Set on the server, carried to the browser in the payload.
  const initial = useState<InitialUser | null>("proteles-initial-user", () => null);
  if (import.meta.server) {
    const { CONTEXT_USER_KEY } = await import("@proteles/vue/server");
    const { cookieNames } = await import("@proteles/bff");
    const user = (useRequestEvent()?.context[CONTEXT_USER_KEY] as InitialUser["user"] | undefined) ?? null;
    // No user but a session cookie: an expired access token (or a dead
    // session) that only /api/auth/me can settle. Render "loading" rather than
    // "signed out" until it has. The name follows PROTELES_COOKIE_SECURE, as the
    // server middleware's own configuration does.
    initial.value = { user, known: user !== null || !useCookie(cookieNames().session).value };
  }
  nuxtApp.vueApp.use(protelesAuth({ initialUser: initial.value?.known ? (initial.value.user as never) : undefined }));
});

A session cookie the middleware could not read as live (an expired access token) renders as "loading" until /api/auth/me has refreshed it, rather than as "signed out". cookieNames comes from @proteles/bff, a dependency of this package; list it in your own package.json to import it, as the quickstart does. It gives __Host-proteles_session whenever cookies are Secure and proteles_session when PROTELES_COOKIE_SECURE=false; if you set cookieSecure in code rather than the environment, pass the same value.

Then use the components anywhere:

<script setup>
import { SignedIn, SignedOut, SignInButton, UserButton, useUser } from "@proteles/vue";

const { user } = useUser();
</script>

<template>
  <SignedOut>
    <SignInButton />
  </SignedOut>

  <SignedIn>
    Hi {{ user?.email }} <UserButton />
  </SignedIn>
</template>

Plain Vue (no Nuxt)

// main.ts
import { createApp } from "vue";
import { protelesAuth } from "@proteles/vue";
import App from "./App.vue";

createApp(App).use(protelesAuth()).mount("#app");

You still need a server serving /api/auth/* — use @proteles/bff directly in whatever backend you have.

Client API

protelesAuth(options?) — the plugin

The idiomatic install. Provides auth to every component and revalidates against /api/auth/me once mounted (so a stale initialUser — e.g. after a logout in another tab — self-corrects).

Option Notes
initialUser From the server. Omit = fetch on mount, null = known signed-out, an object = known signed-in.
basePath BFF mount path. Defaults to VITE_PROTELES_BASE_PATH or /api/auth.

protelesAuth() also returns its auth object, if you want a reference outside components: const { auth } = protelesAuth().

<AuthProvider>

The component alternative to the plugin — use it to scope auth to a subtree, or where you don't control the app instance. Takes the same props.

useUser() / useAuth()

<script setup>
import { useAuth } from "@proteles/vue";

const { user, isLoading, isAuthenticated, signIn, signUp, signOut, reload } = useAuth();
</script>

<template>
  <p v-if="isLoading">Loading…</p>
  <p v-else-if="isAuthenticated">{{ user.sub }}</p>
  <button @click="signIn({ connection: 'google' })">Google</button>
</template>

user, isLoading, isAuthenticated and isUnavailable are refs, so they unwrap in templates and stay reactive in watch/computed. useUser() returns just those four. Both throw a clear error if used without the plugin or an <AuthProvider>.

Components

Component Renders
<SignedIn> its slot when a user is signed in
<SignedOut> its slot when no user is signed in
<Protect> its default slot when signed in, else the fallback slot (nothing while loading)
<SignInButton return-to? connection?> a button that starts login (connection picks a social provider)
<SignUpButton return-to?> a button that starts sign-up (below)
<SignOutButton return-to?> a button that logs out
<UserButton> the user's name/email plus a sign-out control

<SignInButton>/<SignUpButton>/<SignOutButton>/<UserButton> are unstyled by design, and class reaches their root element through Vue's normal attribute inheritance.

That does not apply to <SignedIn>, <SignedOut> and <Protect>: they render their slot directly, so their root is a fragment, and Vue does not inherit attributes onto a fragment root. A class there is dropped and Vue warns about extraneous non-props attributes. Wrap them in an element you style yourself.

<SignedIn>/<SignedOut> render nothing until who is signed in is known — from initialUser, or from the first answer to /api/auth/me, which a busy spell (below) does not give — so there's no signed-in/out flash.

Sign-up

signUp({ returnTo }) and <SignUpButton> send the person through /api/auth/signup to the workspace's hosted sign-up page. They give their email address, follow the link they are mailed, choose a password, and come back to returnTo (by default the page they were on) signed in, as after a sign-in. Where the workspace has not opened its sign-up page, they see the ordinary sign-in page instead.

It takes no connection. For a sign-up through a social provider, use signIn({ connection }): a first sign-in through it makes an account where the workspace allows that.

When the sign-in service is busy

While the authorization server is too busy to refresh a session, the BFF's /api/auth/me answers 503 and keeps the session cookie. That — or a 429, a 408, any other 5xx, or no answer at all — is read as "could not tell", not as signed out: user stays the last one known (isLoading stays true if no answer has come yet), isUnavailable turns true, and /me is asked again after the server's Retry-After (capped at an hour) or, when it named none, after 2 s, 4 s, 8 s and so on up to 30 s. The next answer clears isUnavailable. A 401, or { authenticated: false }, still means signed out, as before.

<SignedIn> keeps rendering for the known user meanwhile; read isUnavailable to tell them the service is busy. An <AuthProvider> that unmounts stops asking, and so does the plugin when its app unmounts (Vue 3.5 and later).

Server API (@proteles/vue/server)

Export Purpose
protelesAuthHandler(event, config?) Handles login/callback/logout/me; return it straight from an h3 handler
getUser(event, config?) The sanitized user (no tokens) — prefers event.context, falls back to the cookie
getSession(event, config?) The full session including tokens — server-side only
guardRequest(event, { publicPaths }, config?) A redirect Response when the request should bounce to login, else undefined
toWebRequest(source) The adapter: accepts a Web Request, { request }, { web: { request } }, or an h3 event's node.req

publicPaths entries match exactly or as a path-segment prefix: "/blog" covers /blog and /blog/post but not /blogging. "/" is exact-only, so listing the home page doesn't accidentally make the whole app public.

The auth endpoints are driven entirely by the URL, method, and cookie header — none reads a request body — so toWebRequest never consumes one, and a body your framework needs downstream stays intact.

Inviting people into your app

Register <your app>/api/auth/login as the app's sign-in start address (its initiate_login_uri). An invitation sent through the Management API with the app's client_id then sends the person who accepts it to that route with iss, and the sign-in it starts finds the account they just made already signed in. The route checks iss against PROTELES_ISSUER and always signs in at PROTELES_ISSUER, whatever iss names.

Configuration (environment)

Server-side, read by the handlers — same names as every Proteles BFF package:

Variable Required Default
PROTELES_ISSUER ✅
PROTELES_CLIENT_ID ✅
PROTELES_CLIENT_SECRET (omit for a public PKCE client)
PROTELES_SESSION_SECRET ✅ base64 of 32 random bytes
PROTELES_REDIRECT_URI ✅* * or derived from PROTELES_APP_URL
PROTELES_APP_URL
PROTELES_SCOPES openid profile email offline_access
PROTELES_COOKIE_SECURE true
PROTELES_BASE_PATH /api/auth

Client-side, only VITE_PROTELES_BASE_PATH is read (and only if you moved the routes). Generate a session secret:

node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Authoring note

The components are defineComponent render functions rather than .vue SFCs. They're tiny and headless, so this keeps the package a plain TypeScript build — no SFC compiler, no bundler — and directly unit-testable. From the outside nothing changes: you use <SignedIn> in your own SFCs as normal.

Develop

npm install      # from the repository root
npm run build    # tsc -> dist
npm test         # tsx + node:test

Tests cover the reactive auth object, the components (server-rendered via @vue/server-renderer — no jsdom), and the Nuxt/h3 bindings including the event-to-Request adapter. The full loop also runs against a live server via scripts/verify-nuxt.mjs (run by Proteles' integration tests).

Related Packages

oidc-js-vue

Simple OIDC authentication for Vue. Plugin, composables, and navigation guards w...

1,013 3
@cboxdk/id-nuxt

Nuxt module for Cbox ID — drop-in sign-in / callback / sign-out routes and sessi...

1,087 0
@authaction/server-sdk

AuthAction server-side SDK — session-based auth for Next.js, Nuxt, Remix, and Sv...

734
@mbsoftlabs/nuxt-oidc-auth

Nuxt 4 module for OIDC/SSO authentication with Laravel Passport or any OIDC prov...

4,296 0

Version History

Version Released Vue License
0.4.0 ^3.3.0 MIT
0.3.0 ^3.3.0 MIT
0.2.0 ^3.3.0 MIT
0.1.1 ^3.3.0 MIT
0.1.0 ^3.3.0 MIT