Skip to content
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

feat: implement refresh provider schema #581

Merged
merged 17 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 30 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,36 @@ jobs:
# start prod-app and curl from it
- run: "timeout 60 pnpm start & (sleep 45 && curl --fail localhost:3000)"

test-playground-refresh:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./playground-refresh
steps:
- uses: actions/checkout@v3

- name: Use Node.js 16.14.2
uses: actions/setup-node@v3
with:
node-version: 16.14.2

- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8

# Install deps
- run: pnpm i

# Check building
- run: pnpm build

# start prod-app and curl from it
- run: "timeout 60 pnpm start & (sleep 45 && curl --fail localhost:$PORT)"
env:
AUTH_ORIGIN: "http://localhost:3002"
PORT: 3002

test-playground-authjs:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -97,5 +126,5 @@ jobs:
# start prod-app and curl from it
- run: "timeout 60 pnpm start & (sleep 45 && curl --fail localhost:$PORT)"
env:
AUTH_ORIGIN: 'http://localhost:3001'
AUTH_ORIGIN: "http://localhost:3001"
PORT: 3001
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"prepack": "nuxt-module-build",
"build": "nuxi build",
"lint": "eslint . --max-warnings=0",
"clean": "rm -rf playground-authjs/.nuxt playground-local/.nuxt dist .nuxt",
"clean": "rm -rf playground-authjs/.nuxt playground-local/.nuxt playground-refresh/.nuxt dist .nuxt",
"typecheck": "nuxi prepare playground-local && tsc --noEmit",
"typecheck:refresh": "nuxi prepare playground-refresh && tsc --noEmit",
"dev:prepare": "nuxt-module-build --stub"
},
"dependencies": {
Expand Down
57 changes: 57 additions & 0 deletions playground-refresh/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script lang="ts" setup>
import { ref } from "vue";
import { useAuth } from "#imports";
const {
signIn,
token,
refreshToken,
refresh,
data,
status,
lastRefreshedAt,
signOut,
getSession,
} = useAuth();
const username = ref("hunter");
const password = ref("hunter2");
</script>

<template>
<div>
<pre>Status: {{ status }}</pre>
<pre>Data: {{ data || "no session data present, are you logged in?" }}</pre>
<pre>Last refreshed at: {{ lastRefreshedAt || "no refresh happened" }}</pre>
<pre>JWT token: {{ token || "no token present, are you logged in?" }}</pre>
<pre>
JWT refreshToken: {{
refreshToken || "no refreshToken present, are you logged in?"
}}</pre
>
<form @submit.prevent="signIn({ username, password })">
<input v-model="username" type="text" placeholder="Username" />
<input v-model="password" type="password" placeholder="Password" />
<button type="submit">sign in</button>
</form>
<br />
<button
@click="
signIn({ username, password }, { callbackUrl: '/protected/globally' })
"
>
sign in (with redirect to protected page)
</button>
<br />
<button @click="signOut({ callbackUrl: '/signout' })">sign out</button>
<br />
<button @click="getSession({ required: false })">
refresh session (required: false)
</button>
<br />
<button @click="getSession({ required: true, callbackUrl: '/' })">
refresh session (required: true)
</button>
<br />
<button @click="refresh()">refresh tokens</button>
<NuxtPage />
</div>
</template>
28 changes: 28 additions & 0 deletions playground-refresh/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export default defineNuxtConfig({
modules: ["../src/module.ts"],
build: {
transpile: ["jsonwebtoken"],
},
auth: {
provider: {
type: "refresh",
endpoints: {
getSession: { path: "/user" },
refresh: { path: "/refresh", method: "post" },
},
pages: {
login: "/",
},
token: {
signInResponseTokenPointer: "/token/accessToken",
maxAgeInSeconds: 60 * 5, // 5 min
},
refreshToken: {
signInResponseRefreshTokenPointer: "/token/refreshToken",
},
},
globalAppMiddleware: {
isEnabled: true,
},
},
});
24 changes: 24 additions & 0 deletions playground-refresh/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"private": true,
"name": "nuxt-auth-playground-local",
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint . --max-warnings=0",
"dev": "nuxi prepare && nuxi dev",
"build": "nuxi build",
"start": "nuxi preview",
"generate": "nuxi generate",
"postinstall": "nuxt prepare"
},
"dependencies": {
"jsonwebtoken": "^9.0.0",
"zod": "^3.21.4"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.1",
"eslint": "^8.37.0",
"nuxt": "^3.4.2",
"typescript": "^5.0.3",
"vue-tsc": "^1.2.0"
}
}
10 changes: 10 additions & 0 deletions playground-refresh/pages/always-unprotected.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<template>
<div>I'm always public, even when the global auth middleware is enabled!</div>
</template>

<script setup lang="ts">
import { definePageMeta } from "#imports";
definePageMeta({
auth: false,
});
</script>
15 changes: 15 additions & 0 deletions playground-refresh/pages/guest.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { definePageMeta } from "#imports";
definePageMeta({
auth: {
unauthenticatedOnly: true,
navigateAuthenticatedTo: "/protected/globally",
},
});
</script>

<template>
<div>
<p>Hey!</p>
</div>
</template>
24 changes: 24 additions & 0 deletions playground-refresh/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { definePageMeta } from "#imports";
definePageMeta({ auth: false });
</script>

<template>
<div>
<nuxt-link to="/"> -> manual login, logout, refresh button </nuxt-link>
<br />
<nuxt-link to="/protected/globally"> -> globally protected page </nuxt-link>
<br />
<nuxt-link to="/protected/locally">
-> locally protected page (only works if global middleware disabled)
</nuxt-link>
<br />
<nuxt-link to="/always-unprotected">
-> page that is always unprotected
</nuxt-link>
<br />
<nuxt-link to="/guest"> -> guest mode </nuxt-link>
<br />
<div>select one of the above actions to get started.</div>
</div>
</template>
6 changes: 6 additions & 0 deletions playground-refresh/pages/protected/globally.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<div>
I'm a secret! My protection works via a global middleware. If you turned off
the global middleware, then I'll also be visible without authentication :(
</div>
</template>
14 changes: 14 additions & 0 deletions playground-refresh/pages/protected/locally.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<div>
I'm a secret! My protection works via the named `nuxt-auth` module
middleware.
</div>
</template>

<script setup lang="ts">
import { definePageMeta } from "#imports";
// Note: This is only for testing, it does not make sense to do this with `globalAppMiddleware` turned on
definePageMeta({
middleware: "auth",
});
</script>
8 changes: 8 additions & 0 deletions playground-refresh/pages/signout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<div>You just signed out!</div>
</template>

<script setup lang="ts">
import { definePageMeta } from "#imports";
definePageMeta({ auth: false });
</script>
Binary file added playground-refresh/public/favicon.ico
Binary file not shown.
40 changes: 40 additions & 0 deletions playground-refresh/server/api/auth/login.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import z from "zod";
import { sign } from "jsonwebtoken";

export const SECRET = "dummy";

export default eventHandler(async (event) => {
const result = z
.object({ username: z.string().min(1), password: z.literal("hunter2") })
.safeParse(await readBody(event));
if (!result.success) {
throw createError({
statusCode: 403,
statusMessage: "Unauthorized, hint: try `hunter2` as password",
});
}

const expiresIn = 15;

const { username } = result.data;

const user = {
username,
picture: "https://github.com/nuxt.png",
name: "User " + username,
};

const accessToken = sign({ ...user, scope: ["test", "user"] }, SECRET, {
expiresIn,
});
const refreshToken = sign({ ...user, scope: ["test", "user"] }, SECRET, {
expiresIn: 60 * 60 * 24,
});

return {
token: {
accessToken,
refreshToken,
},
};
});
1 change: 1 addition & 0 deletions playground-refresh/server/api/auth/logout.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default eventHandler(() => ({ status: "OK " }));
56 changes: 56 additions & 0 deletions playground-refresh/server/api/auth/refresh.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { sign, verify } from "jsonwebtoken";

export const SECRET = "dummy";

interface User {
username: string;
name: string;
picture: string;
}

interface JwtPayload extends User {
scope: Array<"test" | "user">;
exp: number;
}

export default eventHandler(async (event) => {
const body = await readBody<{ refreshToken: string }>(event);

if (!body.refreshToken) {
throw createError({
statusCode: 403,
statusMessage: "Unauthorized, no refreshToken in payload",
});
}

const decoded = verify(body.refreshToken, SECRET) as JwtPayload | undefined;

if (!decoded) {
throw createError({
statusCode: 403,
statusMessage: "Unauthorized, refreshToken can`t be verified",
});
}

const expiresIn = 60 * 5; // 5 minutes

const user: User = {
username: decoded.username,
picture: decoded.picture,
name: decoded.name,
};

const accessToken = sign({ ...user, scope: ["test", "user"] }, SECRET, {
expiresIn,
});
const refreshToken = sign({ ...user, scope: ["test", "user"] }, SECRET, {
expiresIn: 60 * 60 * 24,
});

return {
token: {
accessToken,
refreshToken,
},
};
});
37 changes: 37 additions & 0 deletions playground-refresh/server/api/auth/user.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { H3Event } from "h3";
import { verify } from "jsonwebtoken";
import { SECRET } from "./login.post";

const TOKEN_TYPE = "Bearer";

const extractToken = (authHeaderValue: string) => {
const [, token] = authHeaderValue.split(`${TOKEN_TYPE} `);
return token;
};

const ensureAuth = (event: H3Event) => {
const authHeaderValue = getRequestHeader(event, "authorization");
if (typeof authHeaderValue === "undefined") {
throw createError({
statusCode: 403,
statusMessage:
"Need to pass valid Bearer-authorization header to access this endpoint",
});
}

const extractedToken = extractToken(authHeaderValue);
try {
return verify(extractedToken, SECRET);
} catch (error) {
console.error("Login failed. Here's the raw error:", error);
throw createError({
statusCode: 403,
statusMessage: "You must be logged in to use this endpoint",
});
}
};

export default eventHandler((event) => {
const user = ensureAuth(event);
return user;
});
4 changes: 4 additions & 0 deletions playground-refresh/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./.nuxt/tsconfig.json",
"exclude": ["../docs"]
}
Loading
Loading