Skip to content

Commit

Permalink
feat: add hashPassword & verifyPassword server utils
Browse files Browse the repository at this point in the history
* feat: support password

* [autofix.ci] apply automated fixes

* feat(playground): add login endpoint

* chore: refactor

* up

* lint

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Sébastien Chopin <[email protected]>
  • Loading branch information
3 people authored Sep 25, 2024
1 parent 7431e56 commit 0c4d050
Show file tree
Hide file tree
Showing 14 changed files with 709 additions and 69 deletions.
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,34 @@ Make sure to set the callback URL in your OAuth app settings as `<your-domain>/a

If the redirect URL mismatch in production, this means that the module cannot guess the right redirect URL. You can set the `NUXT_OAUTH_<PROVIDER>_REDIRECT_URL` env variable to overwrite the default one.

### Password Utils

Nuxt Auth Utils provides a `hashPassword` and `verifyPassword` function to hash and verify passwords by using [scrypt](https://en.wikipedia.org/wiki/Scrypt) as it is supported in many JS runtime.

```ts
const hashedPassword = await hashPassword('user_password')

if (await verifyPassword(hashedPassword, 'user_password')) {
// Password is valid
}
```

You can configure the scrypt options in your `nuxt.config.ts`:

```ts
export default defineNuxtConfig({
modules: ['nuxt-auth-utils'],
auth: {
hash: {
scrypt: {
// See https://github.com/adonisjs/hash/blob/94637029cd526783ac0a763ec581306d98db2036/src/types.ts#L144
}
}
}
})
```


### Extend Session

We leverage hooks to let you extend the session data with your own data or log when the user clears the session.
Expand Down Expand Up @@ -414,7 +442,7 @@ npm run release
[npm-version-href]: https://npmjs.com/package/nuxt-auth-utils

[npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-auth-utils.svg?style=flat&colorA=020420&colorB=00DC82
[npm-downloads-href]: https://npmjs.com/package/nuxt-auth-utils
[npm-downloads-href]: https://npm.chart.dev/nuxt-auth-utils

[license-src]: https://img.shields.io/npm/l/nuxt-auth-utils.svg?style=flat&colorA=020420&colorB=00DC82
[license-href]: https://npmjs.com/package/nuxt-auth-utils
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "nuxt-auth-utils",
"version": "0.3.9",
"description": "Minimalist Auth module for Nuxt with SSR",
"description": "Add Authentication to Nuxt applications with secured & sealed cookies sessions.",
"repository": "Atinux/nuxt-auth-utils",
"license": "MIT",
"type": "module",
Expand Down Expand Up @@ -32,7 +32,8 @@
"test:watch": "vitest watch"
},
"dependencies": {
"@nuxt/kit": "^3.13.2",
"@adonisjs/hash": "^9.0.5",
"@nuxt/kit": "^3.13.0",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"ofetch": "^1.3.4",
Expand Down
6 changes: 4 additions & 2 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const toast = useToast()
async function login() {
if (logging.value || !password.value) return
logging.value = true
await $fetch('/api/login', {
await $fetch('/api/built-in-password', {
method: 'POST',
body: {
password: password.value,
Expand Down Expand Up @@ -180,13 +180,15 @@ const providers = computed(() =>
<template
#default="{ loggedIn, clear }"
>
<AuthRegister v-if="!loggedIn" />
<AuthLogin v-if="!loggedIn" />
<UButton
v-if="!loggedIn"
size="xs"
color="gray"
@click="loginModal = true"
>
Login
Login with built-in password
</UButton>
<UDropdown :items="[providers]">
<UButton
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
declare module '#auth-utils' {
interface User {
email?: string
password?: string
spotify?: string
github?: string
Expand Down
71 changes: 71 additions & 0 deletions playground/components/AuthLogin.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script lang="ts" setup>
const isOpen = ref(false)
const { fetch } = useUserSession()
const toast = useToast()
async function login(event: SubmitEvent) {
const target = event.target as HTMLFormElement
await $fetch('/api/login', {
method: 'POST',
body: {
email: target.email.value,
password: target.password.value,
},
}).then(() => {
fetch()
isOpen.value = false
toast.add({
color: 'green',
title: 'User logged in successfully',
})
}).catch((err) => {
console.log(err)
toast.add({
color: 'red',
title: err.data?.message || err.message,
})
})
}
</script>

<template>
<UButton
size="xs"
color="gray"
@click="isOpen = true"
>
Login
</UButton>

<UDashboardModal
v-model="isOpen"
title="Login"
description="Enter your email and password"
>
<form @submit.prevent="login($event)">
<UFormGroup label="Email">
<UInput
name="email"
type="email"
/>
</UFormGroup>
<UFormGroup label="Password">
<UInput
name="password"
type="password"
/>
</UFormGroup>
<UButton
type="submit"
color="black"
class="mt-2"
>
Login
</UButton>
</form>
</UDashboardModal>
</template>
71 changes: 71 additions & 0 deletions playground/components/AuthRegister.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script lang="ts" setup>
const isOpen = ref(false)
const { fetch } = useUserSession()
const toast = useToast()
async function register(event: SubmitEvent) {
const target = event.target as HTMLFormElement
await $fetch('/api/register', {
method: 'POST',
body: {
email: target.email.value,
password: target.password.value,
},
}).then(() => {
fetch()
isOpen.value = false
toast.add({
color: 'green',
title: 'User registered successfully',
})
}).catch((err) => {
console.log(err)
toast.add({
color: 'red',
title: err.data?.message || err.message,
})
})
}
</script>

<template>
<UButton
size="xs"
color="gray"
@click="isOpen = true"
>
Register
</UButton>

<UDashboardModal
v-model="isOpen"
title="Register"
description="Enter your email and password"
>
<form @submit.prevent="register($event)">
<UFormGroup label="Email">
<UInput
name="email"
type="email"
/>
</UFormGroup>
<UFormGroup label="Password">
<UInput
name="password"
type="password"
/>
</UFormGroup>
<UButton
type="submit"
color="black"
class="mt-2"
>
Register
</UButton>
</form>
</UDashboardModal>
</template>
8 changes: 5 additions & 3 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ export default defineNuxtConfig({
extends: ['@nuxt/ui-pro'],
modules: ['nuxt-auth-utils', '@nuxt/ui'],
auth: {},
ui: {
icons: ['simple-icons', 'gravity-ui'],
},
devtools: { enabled: true },
imports: {
autoImport: true,
},
nitro: {
experimental: {
database: true,
},
},
routeRules: {
'/': {
// prerender: true,
Expand Down
6 changes: 4 additions & 2 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
},
"dependencies": {
"nuxt": "^3.13.2",
"nuxt-auth-utils": "latest"
"nuxt-auth-utils": "latest",
"zod": "^3.23.8"
},
"devDependencies": {
"@iconify-json/gravity-ui": "^1.2.1"
"@iconify-json/gravity-ui": "^1.2.1",
"better-sqlite3": "^11.2.1"
}
}
18 changes: 18 additions & 0 deletions playground/server/api/built-in-password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default eventHandler(async (event) => {
const { password } = await readBody(event)

if (password !== '123456') {
throw createError({
statusCode: 401,
message: 'Wrong password',
})
}
await setUserSession(event, {
user: {
password: 'admin',
},
loggedInAt: Date.now(),
})

return {}
})
55 changes: 41 additions & 14 deletions playground/server/api/login.post.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
export default eventHandler(async (event) => {
const { password } = await readBody(event)
import { z } from 'zod'

if (password !== '123456') {
throw createError({
statusCode: 401,
message: 'Wrong password',
})
}
await setUserSession(event, {
user: {
password: 'admin',
},
loggedInAt: Date.now(),
interface DBUser {
id: number
email: string
password: string
}

export default defineLazyEventHandler(async () => {
const db = useDatabase()

await db.sql`CREATE TABLE IF NOT EXISTS users ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "email" TEXT UNIQUE NOT NULL, "password" TEXT NOT NULL)`

const invalidCredentialsError = createError({
statusCode: 401,
// This message is intentionally vague to prevent user enumeration attacks.
message: 'Invalid credentials',
})

return {}
return defineEventHandler(async (event) => {
const { email, password } = await readValidatedBody(event, z.object({
email: z.string().email(),
password: z.string().min(8),
}).parse)

const user = await db.sql<{ rows: DBUser[] }>`SELECT * FROM users WHERE email = ${email}`.then(result => result.rows[0])

if (!user) {
throw invalidCredentialsError
}

if (!(await verifyPassword(user.password, password))) {
throw invalidCredentialsError
}

await setUserSession(event, {
user: {
email,
},
loggedInAt: Date.now(),
})

return setResponseStatus(event, 201)
})
})
29 changes: 29 additions & 0 deletions playground/server/api/register.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from 'zod'

export default defineLazyEventHandler(async () => {
const db = useDatabase()

await db.sql`CREATE TABLE IF NOT EXISTS users ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "email" TEXT UNIQUE NOT NULL, "password" TEXT NOT NULL)`

return defineEventHandler(async (event) => {
const { email, password } = await readValidatedBody(event, z.object({
email: z.string().email(),
password: z.string().min(8),
}).parse)

const hashedPassword = await hashPassword(password)

await db.sql`INSERT INTO users(email, password) VALUES (${email}, ${hashedPassword})`

// In real applications, you should send a confirmation email to the user before logging them in.

await setUserSession(event, {
user: {
email,
},
loggedInAt: Date.now(),
})

return setResponseStatus(event, 201)
})
})
Loading

0 comments on commit 0c4d050

Please sign in to comment.