├── .npmrc ├── docs ├── .npmrc ├── tsconfig.json ├── content │ ├── 2.provider │ │ ├── _dir.yaml │ │ ├── 5.github.md │ │ ├── 10.zitadel.md │ │ ├── 3.aws-cognito.md │ │ ├── 99.oidc.md │ │ ├── 1.index.md │ │ ├── 9.paypal.md │ │ ├── 2.auth0.md │ │ ├── 6.keycloak.md │ │ └── 8.microsoft.md │ ├── 1.getting-started │ │ ├── _dir.yml │ │ ├── 4.providers.md │ │ ├── 2.installation.md │ │ ├── 1.index.md │ │ └── 3.security.md │ ├── 3.server-utils │ │ ├── _dir.yaml │ │ ├── 3.oidc-handlers.md │ │ ├── 2.session-management.md │ │ └── 1.middleware.md │ ├── changelog │ │ └── index.yml │ ├── 99.contributing.md │ ├── index.yml │ ├── 6.hooks.md │ ├── 5.dev-mode.md │ └── 5.single-sign-out.md ├── server │ ├── tsconfig.json │ └── api │ │ └── search.json.get.ts ├── public │ ├── favicon.png │ ├── social-card.png │ ├── content │ │ ├── auth0-grants.png │ │ ├── paypal-scopes.png │ │ ├── nuxt-oidc-auth.png │ │ ├── paypal-redirect.png │ │ ├── microsoft-platform.png │ │ ├── microsoft-scopes.png │ │ ├── paypal-enable_login.png │ │ ├── keycloak-logoutredirect.png │ │ └── microsoft-accounttype.png │ └── favicon.svg ├── app │ ├── assets │ │ ├── nuxt-oidc-auth.png │ │ ├── nuxt-oidc-auth-dark.png │ │ ├── nuxt-oidc-auth-header.png │ │ ├── nuxt-oidc-auth.svg │ │ ├── nuxt-oidc-auth-dark.svg │ │ ├── browser.svg │ │ └── landing.svg │ ├── layouts │ │ └── docs.vue │ ├── components │ │ ├── AppFooter.vue │ │ ├── OgImage │ │ │ └── OgImageDocs.vue │ │ ├── AppHeader.vue │ │ └── content │ │ │ └── contentImage.vue │ ├── error.vue │ ├── app.vue │ ├── app.config.ts │ └── pages │ │ ├── [...slug].vue │ │ └── index.vue ├── .editorconfig ├── .gitignore ├── eslint.config.mjs ├── tailwind.config.ts ├── package.json ├── nuxt.config.ts └── staticwebapp.config.json ├── client ├── .nuxtrc ├── tsconfig.json ├── package.json ├── app.vue ├── components │ ├── AuthState.vue │ ├── DevMode.vue │ ├── ProviderConfigs.vue │ └── Secrets.vue └── nuxt.config.ts ├── playground ├── tsconfig.json ├── server │ ├── tsconfig.json │ └── plugins │ │ └── session.ts ├── app.vue ├── auth.d.ts ├── .eslintrc.cjs ├── package.json ├── layouts │ ├── authentication.vue │ └── default.vue ├── .env.example ├── pages │ ├── auth │ │ └── login.vue │ └── index.vue ├── uno.config.ts ├── composables │ └── useProviders.ts └── nuxt.config.ts ├── pnpm-workspace.yaml ├── src ├── runtime │ ├── server │ │ ├── tsconfig.json │ │ ├── api │ │ │ ├── session.delete.ts │ │ │ ├── session.get.ts │ │ │ ├── refresh.post.ts │ │ │ └── sso.ts │ │ ├── utils │ │ │ ├── proxyAgent.ts │ │ │ ├── config.ts │ │ │ └── oidc.ts │ │ └── handler │ │ │ ├── logout.get.ts │ │ │ ├── dev.ts │ │ │ └── login.get.ts │ ├── plugins │ │ ├── session.client.ts │ │ ├── session.server.ts │ │ ├── provideDefaults.ts │ │ └── sso.client.ts │ ├── providers │ │ ├── oidc.ts │ │ ├── index.ts │ │ ├── paypal.ts │ │ ├── github.ts │ │ ├── apple.ts │ │ ├── cognito.ts │ │ ├── zitadel.ts │ │ ├── keycloak.ts │ │ ├── logto.ts │ │ ├── auth0.ts │ │ ├── entra.ts │ │ └── microsoft.ts │ ├── middleware │ │ └── oidcAuth.ts │ └── composables │ │ └── oidcAuth.ts └── devtools.ts ├── test ├── vitest.config.ts ├── fixtures │ ├── oidcApp │ │ ├── package.json │ │ ├── app.vue │ │ ├── pages │ │ │ ├── excluded.vue │ │ │ ├── auth │ │ │ │ └── login.vue │ │ │ └── index.vue │ │ └── nuxt.config.ts │ └── providers.ts ├── utils.test.ts ├── redirect.test.ts ├── singleSignOut.test.ts └── providers.test.ts ├── tsconfig.json ├── .editorconfig ├── .gitignore ├── LICENSE ├── eslint.config.js ├── .vscode └── settings.json ├── playwright.config.ts ├── .github └── workflows │ └── azure-static-web-apps-icy-glacier-0865d4503.yml ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true -------------------------------------------------------------------------------- /client/.nuxtrc: -------------------------------------------------------------------------------- 1 | imports.autoImport=true 2 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | } 4 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - client 4 | - docs 5 | -------------------------------------------------------------------------------- /docs/content/2.provider/_dir.yaml: -------------------------------------------------------------------------------- 1 | title: Provider 2 | icon: i-carbon-block-storage 3 | -------------------------------------------------------------------------------- /docs/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-oidc-auth-client", 3 | "private": true 4 | } 5 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Getting Started 2 | icon: i-carbon-book 3 | -------------------------------------------------------------------------------- /playground/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /docs/content/3.server-utils/_dir.yaml: -------------------------------------------------------------------------------- 1 | title: Server utils 2 | icon: i-carbon-bare-metal-server 3 | -------------------------------------------------------------------------------- /src/runtime/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /client/app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/favicon.png -------------------------------------------------------------------------------- /docs/public/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/social-card.png -------------------------------------------------------------------------------- /test/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({}) 4 | -------------------------------------------------------------------------------- /test/fixtures/oidcApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keycloak", 3 | "type": "module", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/app/assets/nuxt-oidc-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/app/assets/nuxt-oidc-auth.png -------------------------------------------------------------------------------- /docs/public/content/auth0-grants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/content/auth0-grants.png -------------------------------------------------------------------------------- /docs/public/content/paypal-scopes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/content/paypal-scopes.png -------------------------------------------------------------------------------- /docs/app/assets/nuxt-oidc-auth-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/app/assets/nuxt-oidc-auth-dark.png -------------------------------------------------------------------------------- /docs/public/content/nuxt-oidc-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/content/nuxt-oidc-auth.png -------------------------------------------------------------------------------- /docs/public/content/paypal-redirect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/content/paypal-redirect.png -------------------------------------------------------------------------------- /docs/app/assets/nuxt-oidc-auth-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/app/assets/nuxt-oidc-auth-header.png -------------------------------------------------------------------------------- /docs/public/content/microsoft-platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/content/microsoft-platform.png -------------------------------------------------------------------------------- /docs/public/content/microsoft-scopes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/content/microsoft-scopes.png -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /docs/public/content/paypal-enable_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/content/paypal-enable_login.png -------------------------------------------------------------------------------- /playground/auth.d.ts: -------------------------------------------------------------------------------- 1 | declare module '#oidc-auth' { 2 | interface ProviderInfo { 3 | providerName: string 4 | } 5 | } 6 | 7 | export {} 8 | -------------------------------------------------------------------------------- /docs/public/content/keycloak-logoutredirect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/content/keycloak-logoutredirect.png -------------------------------------------------------------------------------- /docs/public/content/microsoft-accounttype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itpropro/nuxt-oidc-auth/HEAD/docs/public/content/microsoft-accounttype.png -------------------------------------------------------------------------------- /test/fixtures/oidcApp/app.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /docs/content/changelog/index.yml: -------------------------------------------------------------------------------- 1 | title: Changelog 2 | to: https://github.com/itpropro/nuxt-oidc-auth/blob/main/CHANGELOG.md 3 | external: true 4 | target: _blank 5 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/4.providers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Providers 3 | description: Built-in tested and preconfigured providers 4 | --- 5 | 6 | See [Providers](/provider) 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | "exclude": [ 4 | "client", 5 | "node_modules", 6 | "playground", 7 | "dist", 8 | "docs" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/runtime/server/api/session.delete.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3' 2 | import { clearUserSession } from '../utils/session' 3 | 4 | export default defineEventHandler(async (event) => { 5 | await clearUserSession(event) 6 | return { loggedOut: true } 7 | }) 8 | -------------------------------------------------------------------------------- /docs/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /docs/server/api/search.json.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3' 2 | // @ts-expect-error untyped 3 | import { serverQueryContent } from '#content/server' 4 | 5 | export default defineEventHandler(async (event: H3Event) => { 6 | return serverQueryContent(event).where({ _type: 'markdown', navigation: { $ne: false } }).find() 7 | }) 8 | -------------------------------------------------------------------------------- /src/runtime/plugins/session.client.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin, useOidcAuth } from '#imports' 2 | import {} from 'nuxt/app' 3 | 4 | export default defineNuxtPlugin(async (nuxt) => { 5 | if (!nuxt.payload.serverRendered) { 6 | nuxt.hook('app:mounted', async () => { 7 | await useOidcAuth().fetch() 8 | }) 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | 26 | # VSC 27 | .history 28 | -------------------------------------------------------------------------------- /src/runtime/providers/oidc.ts: -------------------------------------------------------------------------------- 1 | import type { OidcProviderConfig } from '../server/utils/provider' 2 | import { defineOidcProvider } from '../server/utils/provider' 3 | 4 | type OidcRequiredFields = 'clientId' | 'clientSecret' | 'authorizationUrl' | 'tokenUrl' | 'redirectUri' 5 | 6 | export const oidc = defineOidcProvider() 7 | -------------------------------------------------------------------------------- /src/runtime/plugins/session.server.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin, useOidcAuth } from '#imports' 2 | import {} from 'nuxt/app' 3 | 4 | export default defineNuxtPlugin({ 5 | name: 'session-fetch-plugin', 6 | enforce: 'pre', 7 | async setup(nuxt) { 8 | if (nuxt.payload.serverRendered) { 9 | await useOidcAuth().fetch() 10 | } 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /test/fixtures/oidcApp/pages/excluded.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /playground/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | '@nuxt/eslint-config', 5 | ], 6 | rules: { 7 | // Global 8 | 'semi': ['error', 'never'], 9 | 'quotes': ['error', 'single'], 10 | 'quote-props': ['error', 'as-needed'], 11 | // Vue 12 | 'vue/multi-word-component-names': 0, 13 | 'vue/max-attributes-per-line': 'off', 14 | 'vue/no-v-html': 0, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /docs/content/99.contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contributing 3 | description: How to contribute to nuxt-oidc-auth 4 | --- 5 | 6 | ## Contributing 7 | 8 | ```bash 9 | # Install dependencies 10 | pnpm install 11 | 12 | # Generate type stubs 13 | pnpm run dev:prepare 14 | 15 | # Develop with the playground 16 | pnpm run dev 17 | 18 | # Build the playground 19 | pnpm run dev:build 20 | 21 | # Run ESLint 22 | pnpm run lint 23 | ``` 24 | -------------------------------------------------------------------------------- /src/runtime/providers/index.ts: -------------------------------------------------------------------------------- 1 | export { apple } from './apple.js' 2 | export { auth0 } from './auth0.js' 3 | export { cognito } from './cognito.js' 4 | export { entra } from './entra.js' 5 | export { github } from './github.js' 6 | export { keycloak } from './keycloak.js' 7 | export { logto } from './logto.js' 8 | export { microsoft } from './microsoft.js' 9 | export { oidc } from './oidc.js' 10 | export { paypal } from './paypal.js' 11 | export { zitadel } from './zitadel.js' 12 | -------------------------------------------------------------------------------- /docs/app/layouts/docs.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/runtime/server/api/session.get.ts: -------------------------------------------------------------------------------- 1 | import type { UserSession } from '../../types' 2 | import { defineEventHandler, sendRedirect } from 'h3' 3 | import { getUserSession, sessionHooks } from '../utils/session' 4 | 5 | export default defineEventHandler(async (event) => { 6 | try { 7 | const session = await getUserSession(event) 8 | await sessionHooks.callHookParallel('fetch', session as UserSession, event) 9 | return session || {} 10 | } 11 | catch { 12 | return {} 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/fixtures/oidcApp/pages/auth/login.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oidc-auth-playground", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "preview": "nuxt preview", 9 | "generate": "nuxi generate" 10 | }, 11 | "dependencies": { 12 | "nuxt": "^3.15.2", 13 | "vue": "^3.5.13" 14 | }, 15 | "devDependencies": { 16 | "@iconify-json/majesticons": "^1.2.1", 17 | "@iconify-json/simple-icons": "^1.2.9", 18 | "@nuxtjs/color-mode": "^3.5.1", 19 | "@unocss/nuxt": "^0.63.4", 20 | "nuxt-oidc-auth": "latest" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/app/components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .data 23 | .vercel_build_output 24 | .build-* 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # Intellij idea 37 | *.iml 38 | .idea 39 | 40 | # OSX 41 | .DS_Store 42 | .AppleDouble 43 | .LSOverride 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | oidcstorage 51 | playwright-report 52 | test-results 53 | test/fixtures/oidcApp/output 54 | -------------------------------------------------------------------------------- /client/components/AuthState.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 31 | -------------------------------------------------------------------------------- /src/runtime/server/api/refresh.post.ts: -------------------------------------------------------------------------------- 1 | import type { UserSession } from '../../types' 2 | import { defineEventHandler } from 'h3' 3 | import { getUserSession, refreshUserSession, sessionHooks } from '../utils/session' 4 | 5 | export default defineEventHandler(async (event) => { 6 | try { 7 | let session = await getUserSession(event) 8 | if (session) { 9 | // Handle, if getUserSession already refreshed it 10 | if (session.updatedAt && session.updatedAt > Date.now() / 1000 - 100) { 11 | session = await refreshUserSession(event) as UserSession 12 | } 13 | await sessionHooks.callHookParallel('refresh', session, event) 14 | return session 15 | } 16 | } 17 | catch { 18 | return {} 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /src/runtime/providers/paypal.ts: -------------------------------------------------------------------------------- 1 | import { defineOidcProvider, type OidcProviderConfig } from '../server/utils/provider' 2 | 3 | type PayPalRequiredFields = 'clientId' | 'clientSecret' 4 | 5 | export const paypal = defineOidcProvider({ 6 | responseType: 'code', 7 | validateAccessToken: false, 8 | validateIdToken: false, 9 | skipAccessTokenParsing: true, 10 | state: true, 11 | nonce: true, 12 | tokenRequestType: 'form-urlencoded', 13 | scope: ['openid'], 14 | requiredProperties: [ 15 | 'clientId', 16 | 'clientSecret', 17 | 'authorizationUrl', 18 | 'tokenUrl', 19 | 'redirectUri', 20 | ], 21 | authorizationUrl: '', 22 | tokenUrl: '', 23 | userInfoUrl: '', 24 | redirectUri: '', 25 | }) 26 | -------------------------------------------------------------------------------- /client/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'pathe' 2 | 3 | export default defineNuxtConfig({ 4 | ssr: false, 5 | modules: [ 6 | '@nuxt/devtools-ui-kit', 7 | ], 8 | nitro: { 9 | output: { 10 | publicDir: resolve(__dirname, '../dist/client'), 11 | }, 12 | }, 13 | app: { 14 | baseURL: '/__nuxt-oidc-auth', 15 | }, 16 | unocss: { 17 | shortcuts: { 18 | // General Tokens 19 | 'bg-base': 'n-bg-base', 20 | 'bg-active': 'n-bg-active', 21 | 'border-base': 'n-border-base', 22 | 'text-secondary': 'color-black/50 dark:color-white/50', 23 | // Reusable 24 | 'x-divider': 'h-1px w-full bg-gray/15', 25 | }, 26 | }, 27 | experimental: { 28 | componentIslands: true, 29 | }, 30 | compatibilityDate: '2024-09-19', 31 | }) 32 | -------------------------------------------------------------------------------- /src/runtime/server/utils/proxyAgent.ts: -------------------------------------------------------------------------------- 1 | import { ofetch } from 'ofetch' 2 | 3 | export async function getUndiciModule() { 4 | try { 5 | const undici = await import('undici') 6 | return undici 7 | } 8 | catch { 9 | console.warn('Optional dependency \'undici\' is not installed.') 10 | return null 11 | } 12 | } 13 | 14 | export async function getProxyAgentOfetch(proxyUrl: string, ignoreProxyCertificateErrors = false) { 15 | const undici = await getUndiciModule() 16 | if (!undici) { 17 | throw new Error('Cannot create ProxyAgent, undici module is missing.') 18 | } 19 | const proxyAgent = ignoreProxyCertificateErrors ? new undici.ProxyAgent({ uri: proxyUrl, requestTls: { rejectUnauthorized: false } }) : new undici.ProxyAgent({ uri: proxyUrl }) 20 | return ofetch.create({ dispatcher: proxyAgent }) 21 | } 22 | -------------------------------------------------------------------------------- /src/runtime/providers/github.ts: -------------------------------------------------------------------------------- 1 | import type { OidcProviderConfig } from '../server/utils/provider' 2 | import { defineOidcProvider } from '../server/utils/provider' 3 | 4 | type GithubRequiredFields = 'clientId' | 'clientSecret' | 'redirectUri' 5 | 6 | export const github = defineOidcProvider({ 7 | authorizationUrl: 'https://github.com/login/oauth/authorize', 8 | tokenUrl: 'https://github.com/login/oauth/access_token', 9 | userInfoUrl: 'https://api.github.com/user', 10 | tokenRequestType: 'json', 11 | authenticationScheme: 'body', 12 | scope: ['user:email'], 13 | pkce: false, 14 | state: true, 15 | nonce: false, 16 | skipAccessTokenParsing: true, 17 | requiredProperties: [ 18 | 'clientId', 19 | 'clientSecret', 20 | 'authorizationUrl', 21 | 'tokenUrl', 22 | 'redirectUri', 23 | ], 24 | validateAccessToken: false, 25 | validateIdToken: false, 26 | }) 27 | -------------------------------------------------------------------------------- /docs/app/components/OgImage/OgImageDocs.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | -------------------------------------------------------------------------------- /src/runtime/middleware/oidcAuth.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized } from 'vue-router' 2 | import { defineNuxtRouteMiddleware, useOidcAuth, useRuntimeConfig } from '#imports' 3 | 4 | interface MiddlewareOptions { 5 | /** 6 | * Whether to enable the middleware. 7 | * 8 | * @default true 9 | */ 10 | enabled: boolean 11 | } 12 | 13 | declare module '#app' { 14 | interface PageMeta { 15 | oidcAuth?: MiddlewareOptions 16 | } 17 | } 18 | 19 | declare module 'vue-router' { 20 | interface RouteMeta { 21 | oidcAuth?: MiddlewareOptions 22 | } 23 | } 24 | 25 | export default defineNuxtRouteMiddleware(async (to) => { 26 | if (to.meta.oidcAuth?.enabled === false) { 27 | return 28 | } 29 | // 404 exclusion 30 | const isErrorPage = !(to.matched.length > 0) 31 | if (isErrorPage) { 32 | return 33 | } 34 | const { loggedIn, login } = useOidcAuth() 35 | 36 | if (loggedIn.value === true || to.path.startsWith('/auth/')) { 37 | return 38 | } 39 | await login() 40 | }) 41 | -------------------------------------------------------------------------------- /test/fixtures/oidcApp/pages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | -------------------------------------------------------------------------------- /docs/content/3.server-utils/3.oidc-handlers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: OIDC Handler 3 | description: Nuxt OIDC Auth route handlers 4 | --- 5 | 6 | ## OIDC Event Handlers 7 | 8 | All configured providers automatically register the following server routes. 9 | 10 | - `/auth//callback` 11 | - `/auth//login` 12 | - `/auth//logout` 13 | 14 | In addition, if `defaultProvider` is set, the following route rules are registered as forwards to the default provider. 15 | 16 | - `/auth/login` 17 | - `/auth/logout` 18 | 19 | ### Using the session in server side code 20 | 21 | You can access the user session in your server side code by using the `getUserSession` function from the `@nuxtjs/oidc-auth` module. 22 | 23 | ```ts 24 | import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js' 25 | 26 | export default eventHandler(async (event) => { 27 | const session = await getUserSession(event) 28 | return session.userName 29 | }) 30 | ``` 31 | 32 | Be careful to not expose any sensitive information from the handler code. 33 | -------------------------------------------------------------------------------- /src/runtime/providers/apple.ts: -------------------------------------------------------------------------------- 1 | import type { OidcProviderConfig } from '../server/utils/provider' 2 | import { defineOidcProvider } from '../server/utils/provider' 3 | 4 | type AppleRequiredFields = 'clientId' | 'clientSecret' | 'authorizationUrl' | 'tokenUrl' | 'redirectUri' 5 | 6 | export const apple = defineOidcProvider({ 7 | authorizationUrl: 'https://appleid.apple.com/auth/oauth2/v2/authorize', 8 | tokenUrl: 'https://appleid.apple.com/auth/oauth2/v2/token', 9 | userInfoUrl: '', 10 | tokenRequestType: 'json', 11 | responseType: 'code', 12 | authenticationScheme: 'body', 13 | grantType: 'authorization_code', 14 | scope: ['user:email'], 15 | pkce: false, 16 | state: true, 17 | nonce: false, 18 | scopeInTokenRequest: false, 19 | skipAccessTokenParsing: true, 20 | requiredProperties: [ 21 | 'clientId', 22 | 'clientSecret', 23 | 'authorizationUrl', 24 | 'tokenUrl', 25 | 'redirectUri', 26 | ], 27 | validateAccessToken: false, 28 | validateIdToken: false, 29 | }) 30 | -------------------------------------------------------------------------------- /playground/layouts/authentication.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 35 | -------------------------------------------------------------------------------- /docs/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat' 3 | 4 | export default createConfigForNuxt({ 5 | features: { 6 | tooling: false, 7 | standalone: false, 8 | stylistic: true, 9 | }, 10 | }, { 11 | rules: { 12 | 'node/prefer-global/process': 'off', 13 | }, 14 | ignores: ['.github/*'], 15 | }).prepend( 16 | antfu( 17 | { 18 | unocss: false, 19 | markdown: true, 20 | rules: { 21 | 'operator-linebreak': 'off', 22 | 'linebreak-style': ['error', 'unix'], 23 | 'eol-last': ['error', 'always'], 24 | 'node/prefer-global/process': 'off', 25 | 'style/member-delimiter-style': [ 26 | 'error', 27 | { 28 | multiline: { 29 | delimiter: 'none', 30 | requireLast: false, 31 | }, 32 | singleline: { 33 | delimiter: 'semi', 34 | requireLast: false, 35 | }, 36 | }, 37 | ], 38 | }, 39 | }, 40 | ), 41 | ) 42 | -------------------------------------------------------------------------------- /playground/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 35 | -------------------------------------------------------------------------------- /src/runtime/server/api/sso.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from '#imports' 2 | import { createError, createEventStream, defineEventHandler } from 'h3' 3 | import { getSingleSignOutSessionId, getUserSessionId, logoutHooks } from '../utils/session' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const sessionId = await getSingleSignOutSessionId(event) 7 | if (!sessionId) { 8 | throw createError({ 9 | statusCode: 401, 10 | message: 'Unauthorized', 11 | }) 12 | } 13 | const userSessionId = await getUserSessionId(event) 14 | const eventStream = createEventStream(event) 15 | 16 | let logoutHook: () => void 17 | 18 | const cleanupHook = async () => { 19 | await useStorage('oidc').removeItem(userSessionId) 20 | logoutHook() 21 | } 22 | 23 | let firstCall = false 24 | logoutHook = logoutHooks.hook(sessionId, async () => { 25 | if (!firstCall) { 26 | firstCall = true 27 | cleanupHook() 28 | } 29 | await eventStream.push({ 30 | event: 'logout', 31 | data: '', 32 | }) 33 | }) 34 | return eventStream.send() 35 | }) 36 | -------------------------------------------------------------------------------- /playground/server/plugins/session.ts: -------------------------------------------------------------------------------- 1 | export default defineNitroPlugin(() => { 2 | sessionHooks.hook('fetch', async (session) => { 3 | // Extend User Session 4 | // Or throw createError({ ... }) if session is invalid 5 | // session.extended = { 6 | // fromHooks: true 7 | // } 8 | // eslint-disable-next-line no-console 9 | console.log('Injecting "status" claim as test on fetch') 10 | if (!(Object.keys(session).length === 0)) { 11 | const claimToAdd = { status: 'Fetch' } 12 | session.claims = { ...session.claims, ...claimToAdd } 13 | } 14 | }) 15 | 16 | sessionHooks.hook('refresh', async (session) => { 17 | // eslint-disable-next-line no-console 18 | console.log('Injecting "status" claim as test on refresh') 19 | if (!(Object.keys(session).length === 0)) { 20 | const claimToAdd = { status: 'Refresh' } 21 | session.claims = { ...session.claims, ...claimToAdd } 22 | } 23 | }) 24 | 25 | sessionHooks.hook('clear', async () => { 26 | // Log that user logged out 27 | // eslint-disable-next-line no-console 28 | console.log('User logged out') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jan-Henrik Damaschke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | import defaultTheme from 'tailwindcss/defaultTheme' 3 | import plugin from 'tailwindcss/plugin' 4 | 5 | export default >{ 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | sans: ['DM Sans', ...defaultTheme.fontFamily.sans], 10 | }, 11 | colors: { 12 | oidc: { 13 | 200: '010822', 14 | }, 15 | green: { 16 | 50: '#EFFDF5', 17 | 100: '#D9FBE8', 18 | 200: '#B3F5D1', 19 | 300: '#75EDAE', 20 | 400: '#00DC82', 21 | 500: '#00C16A', 22 | 600: '#00A155', 23 | 700: '#007F45', 24 | 800: '#016538', 25 | 900: '#0A5331', 26 | 950: '#052e16', 27 | }, 28 | }, 29 | }, 30 | }, 31 | plugins: [ 32 | plugin(({ addVariant, addUtilities }) => { 33 | addVariant('popover-open', '&:popover-open') 34 | addVariant('starting', '@starting-style') 35 | addUtilities({ 36 | '.transition-discrete': { 37 | transitionBehavior: 'allow-discrete', 38 | }, 39 | }) 40 | }), 41 | ], 42 | } 43 | -------------------------------------------------------------------------------- /src/runtime/providers/cognito.ts: -------------------------------------------------------------------------------- 1 | import { defineOidcProvider, type OidcProviderConfig } from '../server/utils/provider' 2 | 3 | type CognitoRequiredFields = 'baseUrl' | 'clientId' | 'clientSecret' | 'logoutRedirectUri' 4 | 5 | export const cognito = defineOidcProvider({ 6 | userNameClaim: 'username', 7 | tokenRequestType: 'form-urlencoded', 8 | authenticationScheme: 'header', 9 | userInfoUrl: 'oauth2/userInfo', 10 | grantType: 'authorization_code', 11 | scope: ['openid'], 12 | pkce: true, 13 | state: true, 14 | nonce: true, 15 | authorizationUrl: 'oauth2/authorize', 16 | tokenUrl: 'oauth2/token', 17 | logoutUrl: 'logout', 18 | requiredProperties: [ 19 | 'baseUrl', 20 | 'clientId', 21 | 'clientSecret', 22 | 'authorizationUrl', 23 | 'tokenUrl', 24 | 'logoutRedirectUri', 25 | ], 26 | validateAccessToken: false, 27 | validateIdToken: false, 28 | additionalLogoutParameters: { 29 | clientId: '{clientId}', 30 | }, 31 | sessionConfiguration: { 32 | expirationCheck: true, 33 | automaticRefresh: true, 34 | expirationThreshold: 240, 35 | }, 36 | exposeIdToken: true, 37 | logoutRedirectParameterName: 'logout_uri', 38 | }) 39 | -------------------------------------------------------------------------------- /docs/app/error.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 56 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat' 3 | 4 | export default createConfigForNuxt({ 5 | features: { 6 | tooling: true, 7 | standalone: false, 8 | }, 9 | }, { 10 | rules: { 11 | 'node/prefer-global/process': 'off', 12 | }, 13 | ignores: ['.github/**', '**/*.md'], 14 | }).prepend( 15 | antfu( 16 | { 17 | ignores: ['client/', 'docs/'], 18 | unocss: false, 19 | markdown: false, 20 | rules: { 21 | 'operator-linebreak': 'off', 22 | 'linebreak-style': ['error', 'unix'], 23 | 'eol-last': ['error', 'always'], 24 | 'node/prefer-global/process': 'off', 25 | 'style/member-delimiter-style': [ 26 | 'error', 27 | { 28 | multiline: { 29 | delimiter: 'none', 30 | requireLast: false, 31 | }, 32 | singleline: { 33 | delimiter: 'semi', 34 | requireLast: false, 35 | }, 36 | }, 37 | ], 38 | }, 39 | languageOptions: { 40 | parserOptions: { 41 | warnOnUnsupportedTypeScriptVersion: false, 42 | }, 43 | }, 44 | }, 45 | ), 46 | ) 47 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | // Enable the ESlint flat config support 4 | "eslint.useFlatConfig": true, 5 | "prettier.enable": false, 6 | "editor.formatOnSave": false, 7 | "editor.tabSize": 2, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit" 10 | }, 11 | // TypeScript formatter options 12 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, 13 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, 14 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true, 15 | "typescript.format.semicolons": "remove", 16 | "typescript.format.enable": true, 17 | "typescript.preferences.quoteStyle": "single", 18 | "html.format.wrapAttributes": "force-expand-multiline", 19 | "unocss.root": ["playground"], 20 | "eslint.validate": [ 21 | "javascript", 22 | "javascriptreact", 23 | "typescript", 24 | "typescriptreact", 25 | "vue", 26 | "html", 27 | "markdown", 28 | "json", 29 | "jsonc", 30 | "yaml", 31 | "toml", 32 | "xml", 33 | "gql", 34 | "graphql", 35 | "astro", 36 | "svelte", 37 | "css", 38 | "less", 39 | "scss", 40 | "pcss", 41 | "postcss" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /playground/.env.example: -------------------------------------------------------------------------------- 1 | # OIDC MODULE CONFIG 2 | NUXT_OIDC_TOKEN_KEY= 3 | NUXT_OIDC_SESSION_SECRET= 4 | NUXT_OIDC_AUTH_SESSION_SECRET= 5 | # ENTRA ID PROVIDER CONFIG 6 | NUXT_OIDC_PROVIDERS_ENTRA_CLIENT_SECRET= 7 | NUXT_OIDC_PROVIDERS_ENTRA_CLIENT_ID= 8 | NUXT_OIDC_PROVIDERS_ENTRA_AUDIENCE= 9 | NUXT_OIDC_PROVIDERS_ENTRA_AUTHORIZATION_URL= 10 | NUXT_OIDC_PROVIDERS_ENTRA_TOKEN_URL= 11 | NUXT_OIDC_PROVIDERS_ENTRA_USER_NAME_CLAIM= 12 | NUXT_OIDC_PROVIDERS_ENTRA_LOGOUT_URL= 13 | NUXT_OIDC_PROVIDERS_ENTRA_ADDITIONAL_AUTH_PARAMETERS_RESOURCE= 14 | NUXT_OIDC_PROVIDERS_ENTRA_VALIDATE_ACCESS_TOKEN= 15 | NUXT_OIDC_PROVIDERS_ENTRA_VALIDATE_ID_TOKEN= 16 | NUXT_OIDC_PROVIDERS_ENTRA_CALLBACK_REDIRECT_URL= 17 | # AUTH0 PROVIDER CONFIG 18 | NUXT_OIDC_PROVIDERS_AUTH0_CLIENT_SECRET= 19 | NUXT_OIDC_PROVIDERS_AUTH0_CLIENT_ID= 20 | NUXT_OIDC_PROVIDERS_AUTH0_BASE_URL= 21 | # GITHUB PROVIDER CONFIG 22 | NUXT_OIDC_PROVIDERS_GITHUB_CLIENT_SECRET= 23 | NUXT_OIDC_PROVIDERS_GITHUB_CLIENT_ID= 24 | # KEYCLOAK PROVIDER CONFIG 25 | NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET= 26 | NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID= 27 | NUXT_OIDC_PROVIDERS_KEYCLOAK_BASE_URL= 28 | # COGNITO PROVIDER CONFIG 29 | NUXT_OIDC_PROVIDERS_COGNITO_CLIENT_ID= 30 | NUXT_OIDC_PROVIDERS_COGNITO_CLIENT_SECRET= 31 | NUXT_OIDC_PROVIDERS_COGNITO_BASE_URL= 32 | NUXT_OIDC_PROVIDERS_COGNITO_OPEN_ID_CONFIGURATION= -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-oidc-auth-docs", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "private": true, 6 | "description": "Docs for nuxt-oidc-auth", 7 | "authors": "itpropro", 8 | "scripts": { 9 | "dev": "nuxi dev", 10 | "generate": "nuxi generate", 11 | "build": "set NODE_OPTIONS=--disable-warning=DEP0155 --disable-warning=DEP0166 && nuxi build", 12 | "preview": "nuxi preview", 13 | "swadeploy": "dotenv -- swa deploy .output/public --api-location .output/server --api-language \"node\" --api-version \"20\" --env production" 14 | }, 15 | "dependencies": { 16 | "@iconify-json/carbon": "^1.2.3", 17 | "@iconify-json/heroicons": "^1.2.1", 18 | "@iconify-json/simple-icons": "^1.2.9", 19 | "@nuxt/content": "^2.13.4", 20 | "@nuxt/fonts": "^0.10.0", 21 | "@nuxt/image": "^1.8.1", 22 | "@nuxt/scripts": "^0.9.5", 23 | "@nuxt/ui-pro": "^1.4.4", 24 | "@nuxtjs/seo": "^2.0.0-rc.23", 25 | "@vueuse/core": "^11.1.0", 26 | "@vueuse/nuxt": "^11.1.0", 27 | "nuxt": "^3.13.2", 28 | "nuxt-vitalizer": "^0.10.0" 29 | }, 30 | "devDependencies": { 31 | "@antfu/eslint-config": "^3.8.0", 32 | "@azure/static-web-apps-cli": "^2.0.1", 33 | "@nuxt/eslint": "^0.6.0", 34 | "dotenv-cli": "^7.4.2", 35 | "eslint": "^9.13.0", 36 | "typescript": "5.6.3", 37 | "vue-tsc": "^2.1.6" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/app/app.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 59 | -------------------------------------------------------------------------------- /playground/pages/auth/login.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | -------------------------------------------------------------------------------- /test/fixtures/oidcApp/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | import nuxtOidcAuth from '../../../src/module' 3 | 4 | export default defineNuxtConfig({ 5 | modules: [ 6 | nuxtOidcAuth, 7 | ], 8 | 9 | telemetry: { 10 | enabled: false, 11 | }, 12 | 13 | oidc: { 14 | defaultProvider: 'keycloak', 15 | providers: { 16 | keycloak: { 17 | audience: 'account', 18 | clientId: '', 19 | clientSecret: '', 20 | baseUrl: 'http://localhost:8080/realms/nuxt-oidc-test', 21 | redirectUri: 'http://localhost:3000/auth/keycloak/callback', 22 | userNameClaim: 'preferred_username', 23 | allowedCallbackRedirectUrls: [ 24 | 'http://localhost', 25 | 'http://127.0.0.1', 26 | ], 27 | sessionConfiguration: { 28 | singleSignOut: true, 29 | }, 30 | }, 31 | }, 32 | middleware: { 33 | globalMiddlewareEnabled: true, 34 | customLoginPage: true, 35 | }, 36 | }, 37 | 38 | devtools: { 39 | enabled: true, 40 | }, 41 | 42 | imports: { 43 | autoImport: true, 44 | }, 45 | 46 | nitro: { 47 | preset: 'node-server', 48 | storage: { // Local file system storage for demo purposes 49 | oidc: { 50 | driver: 'fs', 51 | base: 'playground/oidcstorage', 52 | }, 53 | }, 54 | }, 55 | 56 | compatibilityDate: '2024-08-28', 57 | }) 58 | -------------------------------------------------------------------------------- /docs/content/3.server-utils/2.session-management.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Session management 3 | description: Nuxt session management 4 | --- 5 | 6 | ## Session expiration and refresh 7 | 8 | Nuxt OIDC Auth automatically checks if the session is expired and refreshes it if necessary. You can disable this behavior by setting `expirationCheck` and `automaticRefresh` to `false` in the `session` configuration. 9 | The session is automatically refreshed when the `session` object is accessed. You can also manually refresh the session using `refresh` from `useOidcAuth` on the client or on the server side by calling `refreshUserSession(event)`. 10 | 11 | Session expiration and refresh is handled completely server side, the exposed properties in the user session are automatically updated. You can theoretically register a hook that overwrites session fields like `loggedInAt`, but this is not recommended and will be overwritten with each refresh. 12 | 13 | ## Using the session in server side code 14 | 15 | You can access the user session in your server side code by using the `getUserSession` function from the `@nuxtjs/oidc-auth` module. 16 | 17 | ```ts 18 | import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js' 19 | 20 | export default eventHandler(async (event) => { 21 | const session = await getUserSession(event) 22 | return session.userName 23 | }) 24 | ``` 25 | 26 | Be careful to not expose any sensitive information from the handler code. 27 | -------------------------------------------------------------------------------- /src/runtime/providers/zitadel.ts: -------------------------------------------------------------------------------- 1 | import { normalizeURL, withHttps, withoutTrailingSlash } from 'ufo' 2 | import { createProviderFetch, defineOidcProvider, type OidcProviderConfig } from '../server/utils/provider' 3 | 4 | type ZitadelRequiredFields = 'baseUrl' | 'clientId' | 'clientSecret' 5 | 6 | export const zitadel = defineOidcProvider({ 7 | tokenRequestType: 'form-urlencoded', 8 | userInfoUrl: 'oidc/v1/userinfo', 9 | scope: ['openid', 'offline_access'], 10 | pkce: true, 11 | state: true, 12 | nonce: true, 13 | authenticationScheme: 'none', 14 | scopeInTokenRequest: true, 15 | authorizationUrl: 'oauth/v2/authorize', 16 | tokenUrl: 'oauth/v2/token', 17 | logoutUrl: 'oidc/v1/end_session', 18 | requiredProperties: [ 19 | 'baseUrl', 20 | 'clientId', 21 | 'clientSecret', 22 | 'authorizationUrl', 23 | 'tokenUrl', 24 | ], 25 | validateAccessToken: false, 26 | validateIdToken: true, 27 | skipAccessTokenParsing: true, 28 | sessionConfiguration: { 29 | expirationCheck: true, 30 | automaticRefresh: true, 31 | expirationThreshold: 1800, 32 | }, 33 | additionalLogoutParameters: { 34 | clientId: '{clientId}', 35 | }, 36 | logoutRedirectParameterName: 'post_logout_redirect_uri', 37 | async openIdConfiguration(config: any) { 38 | const baseUrl = normalizeURL(withoutTrailingSlash(withHttps(config.baseUrl as string))) 39 | const customFetch = await createProviderFetch(config) 40 | return await customFetch(`${baseUrl}/.well-known/openid-configuration`) 41 | }, 42 | excludeOfflineScopeFromTokenRequest: true, 43 | }) 44 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigOptions } from '@nuxt/test-utils/playwright' 2 | import { defineConfig, devices } from '@playwright/test' 3 | import { isCI, isWindows } from 'std-env' 4 | 5 | const devicesToTest = [ 6 | 'Desktop Firefox', 7 | // Test against other common browser engines. 8 | // 'Desktop Firefox', 9 | // 'Desktop Safari', 10 | // Test against mobile viewports. 11 | // 'Pixel 5', 12 | // 'iPhone 12', 13 | // Test against branded browsers. 14 | // { ...devices['Desktop Edge'], channel: 'msedge' }, 15 | // { ...devices['Desktop Chrome'], channel: 'chrome' }, 16 | ] satisfies Array 17 | 18 | /* See https://playwright.dev/docs/test-configuration. */ 19 | export default defineConfig({ 20 | testIgnore: ['**/utils.test.ts'], 21 | testDir: './test', 22 | /* Run tests in files in parallel */ 23 | fullyParallel: true, 24 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 25 | forbidOnly: !!isCI, 26 | /* Retry on CI only */ 27 | retries: isCI ? 2 : 0, 28 | /* Opt out of parallel tests on CI. */ 29 | workers: isCI ? 1 : undefined, 30 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 31 | reporter: 'html', 32 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 33 | use: { 34 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 35 | trace: 'on-first-retry', 36 | }, 37 | projects: devicesToTest.map(p => typeof p === 'string' ? ({ name: p, use: devices[p] }) : p), 38 | }) 39 | -------------------------------------------------------------------------------- /docs/content/2.provider/5.github.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GitHub 3 | description: GitHub provider documentation 4 | icon: i-simple-icons-github 5 | --- 6 | 7 | ## Feature/OIDC support 8 | 9 | ❌  PKCE
10 | ❌  Nonce
11 | ✅  State
12 | ❌  Access Token validation
13 | ❌  ID Token validation
14 | 15 | ## Introduction 16 | 17 | GitHub is not strictly an OIDC provider, but it can be used as one. Make sure that validation is disabled and that you keep the `skipAccessTokenParsing` option to `true`. 18 | 19 | Try to use a [GitHub App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps), not the legacy OAuth app. They don't provide the same level of security, have no granular permissions, don't provide refresh tokens and are not tested. 20 | 21 | Make sure to set the callback URL in your OAuth app settings as `/auth/github`. 22 | 23 | ## Example Configuration 24 | 25 | ::callout{icon="i-carbon-warning-alt" color="amber"} 26 | Never store sensitive values like your client secret in your Nuxt config. Our recommendation is to inject at least client id and client secret via. environment variables. 27 | :: 28 | 29 | ```typescript [nuxt.config.ts] 30 | github: { 31 | redirectUri: 'http://localhost:3000/auth/github/callback', 32 | clientId: '', 33 | clientSecret: '', 34 | filterUserInfo: ['login', 'id', 'avatar_url', 'name', 'email'], 35 | }, 36 | ``` 37 | 38 | ### Environment variables 39 | 40 | Dotenv files are only for (local) development. Use a proper configuration management or injection system in production. 41 | 42 | ```ini [.env] 43 | NUXT_OIDC_PROVIDERS_GITHUB_CLIENT_SECRET=CLIENT_SECRET 44 | NUXT_OIDC_PROVIDERS_GITHUB_CLIENT_ID=CLIENT_ID 45 | ``` 46 | -------------------------------------------------------------------------------- /client/components/DevMode.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 65 | -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment nuxt 2 | 3 | import { describe, expect, it } from 'vitest' 4 | import { generatePkceCodeChallenge, generatePkceVerifier, generateRandomUrlSafeString } from '../src/runtime/server/utils/security' 5 | 6 | describe('security', () => { 7 | const length = 68 8 | it('should generate a valid verifier', () => { 9 | const output = generatePkceVerifier() 10 | expect(output).to.toHaveLength(64) 11 | }) 12 | it('should generate a valid verifier with custom length', () => { 13 | const output = generatePkceVerifier(length) 14 | expect(output).to.toHaveLength(length) 15 | }) 16 | it('should validate the length', () => { 17 | expect(() => generatePkceVerifier(42)).toThrow() 18 | expect(() => generatePkceVerifier(129)).toThrow() 19 | }) 20 | it('should generate a valid challenge', async () => { 21 | const verifier = '9d509f04c574c228491421ddd35f209e6952379d025242dcdd51f7f0' 22 | const challenge = '3PKu5yGD74_vuhAQI6-YRiwomm09qfoy1ZV6naT2L1I' 23 | const output = await generatePkceCodeChallenge(verifier) 24 | expect(output).to.equal(challenge) 25 | }) 26 | it('should generate a valid base64url encoded string', () => { 27 | const output = generateRandomUrlSafeString(length) 28 | expect(output).toHaveLength(length) 29 | expect(output).not.toContain('+') 30 | expect(output).not.toContain('/') 31 | expect(output).not.toContain('=') 32 | }) 33 | }) 34 | 35 | describe('config', () => { 36 | it.todo('should merge arrays correctly') 37 | }) 38 | 39 | describe('oidc', () => { 40 | it.todo('should generate a valid form data request') 41 | it.todo('should correctly transform objects keys to snakeCase') 42 | }) 43 | 44 | describe('session', () => { 45 | it.todo('should expose session hooks') 46 | }) 47 | -------------------------------------------------------------------------------- /docs/content/index.yml: -------------------------------------------------------------------------------- 1 | title: Nuxt OIDC Auth Documentation 2 | description: Seamless OpenID Connect authentication for Nuxt Applications 3 | navigation: false 4 | hero: 5 | title: Nuxt OIDC Auth 6 | description: Create your documentation in seconds with this template! 7 | orientation: horizontal 8 | links: 9 | - label: Get started 10 | icon: i-carbon-arrow-right 11 | trailing: true 12 | to: /getting-started 13 | size: lg 14 | - label: Star on GitHub 15 | icon: i-simple-icons-github 16 | size: lg 17 | color: gray 18 | to: https://github.com/itpropro/nuxt-oidc-auth 19 | target: _blank 20 | code: | 21 | ::code-group 22 | ```bash [pnpm] 23 | pnpm dlx nuxi@latest module add nuxt-oidc-auth 24 | ``` 25 | 26 | ```bash [yarn] 27 | yarn dlx nuxi@latest module add nuxt-oidc-auth 28 | ``` 29 | 30 | ```bash [npm] 31 | npx nuxi@latest module add nuxt-oidc-auth 32 | ``` 33 | :: 34 | features: 35 | title: Seamless Modern Authentication for Nuxt 36 | links: 37 | - label: Get started 38 | icon: i-carbon-rule-locked 39 | trailingIcon: i-carbon-arrow-right 40 | color: gray 41 | to: /getting-started 42 | size: lg 43 | items: 44 | - title: Security first 45 | description: Security is a main focus, from encrypted session to supporting every OIDC security measure. 46 | icon: i-carbon-security 47 | to: /getting-started/security 48 | - title: Preconfigured providers 49 | description: There is a ever extending list of tested provider presets to use in your project. 50 | icon: i-carbon-license-maintenance 51 | to: /provider 52 | - title: Dev mode 53 | description: Easy local development with dev mode 54 | icon: i-carbon-code 55 | to: /dev-mode 56 | -------------------------------------------------------------------------------- /src/runtime/plugins/provideDefaults.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | // @ts-expect-error - Missing Nitro type exports in Nuxt 3 | import { defineNitroPlugin } from '#imports' 4 | import { subtle } from 'uncrypto' 5 | import { arrayBufferToBase64 } from 'undio' 6 | import { generateRandomUrlSafeString } from '../server/utils/security' 7 | 8 | export default defineNitroPlugin(async () => { 9 | if (!process.env.NUXT_OIDC_SESSION_SECRET || process.env.NUXT_OIDC_SESSION_SECRET.length < 48) { 10 | const randomSecret = generateRandomUrlSafeString() 11 | process.env.NUXT_OIDC_SESSION_SECRET = randomSecret 12 | console.warn('[nuxt-oidc-auth]: No session secret set, using a random secret. Please set NUXT_OIDC_SESSION_SECRET in your environment with at least 48 chars.') 13 | console.info(`[nuxt-oidc-auth]: NUXT_OIDC_SESSION_SECRET=${randomSecret}`) 14 | } 15 | if (!process.env.NUXT_OIDC_TOKEN_KEY) { 16 | const randomKey = arrayBufferToBase64(await subtle.exportKey('raw', await subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'])), {}) 17 | process.env.NUXT_OIDC_TOKEN_KEY = randomKey 18 | console.warn('[nuxt-oidc-auth]: No refresh token key set, using a random key. Please set NUXT_OIDC_TOKEN_KEY in your environment. Refresh tokens saved in this session will be inaccessible after a server restart.') 19 | console.info(`[nuxt-oidc-auth]: NUXT_OIDC_TOKEN_KEY=${randomKey}`) 20 | } 21 | if (!process.env.NUXT_OIDC_AUTH_SESSION_SECRET) { 22 | const randomKey = generateRandomUrlSafeString() 23 | process.env.NUXT_OIDC_AUTH_SESSION_SECRET = randomKey 24 | console.warn('[nuxt-oidc-auth]: No auth session secret set, using a random secret. Please set NUXT_OIDC_AUTH_SESSION_SECRET in your environment.') 25 | console.info(`[nuxt-oidc-auth]: NUXT_OIDC_AUTH_SESSION_SECRET=${randomKey}`) 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /src/runtime/plugins/sso.client.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin, onNuxtReady, useOidcAuth } from '#imports' 2 | 3 | export default defineNuxtPlugin(() => { 4 | const { loggedIn, currentProvider, logout, refresh, user } = useOidcAuth() 5 | const sseUrl = '/api/_auth/sso' 6 | let eventSource: EventSource | null = null 7 | let retryTimeout: number | null = null 8 | const retryDelay = 2000 9 | const maxRetries = 5 10 | let retryCount = 0 11 | 12 | const disconnect = () => { 13 | if (eventSource) { 14 | eventSource.close() 15 | eventSource = null 16 | } 17 | if (retryTimeout) { 18 | clearTimeout(retryTimeout) 19 | retryTimeout = null 20 | } 21 | } 22 | 23 | const connect = () => { 24 | const attemptReconnect = () => { 25 | if (retryTimeout) 26 | return 27 | 28 | if (retryCount >= maxRetries) { 29 | return 30 | } 31 | 32 | retryCount++ 33 | retryTimeout = window.setTimeout(connect, retryDelay) 34 | } 35 | 36 | eventSource = new EventSource(sseUrl) 37 | 38 | eventSource.onopen = () => { 39 | if (retryTimeout) { 40 | clearTimeout(retryTimeout) 41 | retryTimeout = null 42 | } 43 | retryCount = 0 44 | } 45 | 46 | eventSource.addEventListener('logout', async () => { 47 | disconnect() 48 | await refresh() 49 | if (loggedIn.value) { 50 | logout(currentProvider.value) 51 | } 52 | }) 53 | 54 | eventSource.onerror = () => { 55 | if (eventSource?.readyState === EventSource.CLOSED) { 56 | attemptReconnect() 57 | } 58 | } 59 | 60 | window.addEventListener('beforeunload', () => { 61 | disconnect() 62 | }) 63 | } 64 | 65 | onNuxtReady(() => { 66 | if (loggedIn.value && user.value?.singleSignOut) { 67 | connect() 68 | } 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-icy-glacier-0865d4503.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | workflow_dispatch: 5 | # push: 6 | # branches: 7 | # - main 8 | # pull_request: 9 | # types: [opened, synchronize, reopened, closed] 10 | # branches: 11 | # - main 12 | 13 | jobs: 14 | build_and_deploy_job: 15 | # if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 16 | runs-on: ubuntu-latest 17 | name: Build and Deploy Job 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | submodules: true 22 | lfs: false 23 | - name: Build And Deploy 24 | id: builddeploy 25 | uses: Azure/static-web-apps-deploy@v1 26 | with: 27 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ICY_GLACIER_0865D4503 }} 28 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 29 | action: upload 30 | app_location: /playground 31 | api_location: playground/.output/server 32 | output_location: .output/public 33 | env: 34 | PRE_BUILD_COMMAND: npm install -g pnpm 35 | CUSTOM_BUILD_COMMAND: pnpm install && pnpm -w run dev:prepare && pnpm -w run dev:build 36 | NODE_VERSION: 18.17.1 37 | 38 | close_pull_request_job: 39 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 40 | runs-on: ubuntu-latest 41 | name: Close Pull Request Job 42 | steps: 43 | - name: Close Pull Request 44 | id: closepullrequest 45 | uses: Azure/static-web-apps-deploy@v1 46 | with: 47 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ICY_GLACIER_0865D4503 }} 48 | action: close 49 | app_location: /playground 50 | -------------------------------------------------------------------------------- /docs/content/6.hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hooks 3 | description: Nuxt hooks to interact with nuxt-oidc-auth 4 | --- 5 | 6 | ## Hooks 7 | 8 | The following hooks are available to extend the default behavior of the OIDC module: 9 | 10 | - `fetch` (Called when a user session is fetched) 11 | - `clear` (Called before a user session is cleared) 12 | - `refresh` (Called before a user session is refreshed) 13 | 14 | ::callout{icon="i-carbon-warning-alt" color="amber"} 15 | Remember to also update the refresh hook if you modify the session, as claims and other fields would otherwise be wiped. 16 | :: 17 | 18 | #### Example 19 | 20 | ```ts 21 | export default defineNitroPlugin(() => { 22 | sessionHooks.hook('fetch', async (session) => { 23 | // Extend User Session 24 | // Or throw createError({ ... }) if session is invalid 25 | // session.extended = { 26 | // fromHooks: true 27 | // } 28 | console.log('Injecting "status" claim as test') 29 | if (!(Object.keys(session).length === 0)) { 30 | const claimToAdd = { status: 'Fetch' } 31 | session.claims = { ...session.claims, ...claimToAdd } 32 | } 33 | }) 34 | 35 | sessionHooks.hook('refresh', async (session) => { 36 | console.log('Injecting "status" claim as test on refresh') 37 | if (!(Object.keys(session).length === 0)) { 38 | const claimToAdd = { status: 'Refresh' } 39 | session.claims = { ...session.claims, ...claimToAdd } 40 | } 41 | }) 42 | 43 | sessionHooks.hook('clear', async (session) => { 44 | // Log that user logged out 45 | console.log('User logged out') 46 | }) 47 | }) 48 | ``` 49 | 50 | You can theoretically register a hook that overwrites internal session fields like `loggedInAt`, but this is not recommended as it has an impact on the `loggedIn` state of your session. It will not impact the server side refresh and expiration logic, but will be overwritten with each refresh. 51 | -------------------------------------------------------------------------------- /playground/uno.config.ts: -------------------------------------------------------------------------------- 1 | // @unocss-include 2 | 3 | import { 4 | defineConfig, 5 | presetIcons, 6 | presetUno, 7 | presetWebFonts, 8 | transformerDirectives, 9 | transformerVariantGroup, 10 | } from 'unocss' 11 | 12 | export default defineConfig({ 13 | shortcuts: [ 14 | { 15 | 'bg-base': 'bg-white dark:bg-gray-900', 16 | 'font-initial': 'font-alexandria text-sm font-400', 17 | 'font-base': 'text-gray-950 dark:text-gray-100 font-initial', 18 | 'font-base-reverse': 'dark:text-gray-950 text-gray-100 font-initial', 19 | 'btn-base': 'whitespace-nowrap flex justify-center items-center h-10 px-4 border-1 bg-gray-100 dark:bg-gray-800 border-gray-400 text-sm leading-4 rounded-2 transition ease-in-out duration-150 disabled:cursor-not-allowed disabled:(text-black dark:text-white) disabled:opacity-50', 20 | 'btn-login': 'w-60 hover:enabled:bg-gray dark:hover:enabled:text-gray-900 hover:enabled:text-white hover:enabled:drop-shadow-[0_2px_5px_rgba(160,160,160,0.6)] active:enabled:filter-none', 21 | }, 22 | ], 23 | presets: [ 24 | presetUno({ 25 | dark: 'class', 26 | }), 27 | presetIcons({ 28 | warn: true, 29 | extraProperties: { 30 | 'display': 'inline-block', 31 | 'vertical-align': 'middle', 32 | }, 33 | }), 34 | presetWebFonts({ 35 | provider: 'bunny', 36 | fonts: { 37 | sans: 'Alexandria', 38 | serif: 'PT Serif', 39 | mono: 'Roboto Mono', 40 | alexandria: [ 41 | { 42 | name: 'Alexandria', 43 | weights: ['300', '400', '700'], 44 | italic: true, 45 | }, 46 | { 47 | name: 'sans-serif', 48 | provider: 'none', 49 | }, 50 | ], 51 | }, 52 | }), 53 | ], 54 | transformers: [ 55 | transformerDirectives(), 56 | transformerVariantGroup(), 57 | ], 58 | }) 59 | -------------------------------------------------------------------------------- /docs/content/3.server-utils/1.middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Middleware 3 | description: Nuxt middleware integration 4 | --- 5 | 6 | ## Middleware 7 | 8 | This module can automatically add a global middleware to your Nuxt server. You can enable it by setting `globalMiddlewareEnabled` under the `middleware` section of the config. 9 | The middleware automatically redirects all requests to `/auth/login` if the user is not logged in. You can disable this behavior by setting `redirect` to `false` in the `middleware` configuration. 10 | The `/auth/login` route is only configured if you have defined a default provider. If you want to use a custom login page and keep your default provider or don't want to set a default provider at all, you can set `customLoginPage` to `true` in the `middleware` configuration. 11 | 12 | If you set `customLoginPage` to `true`, you have to manually add a login page to your Nuxt app under `/auth/login`. You can use the `login` method from the `useOidcAuth` composable to redirect the user to the respective provider login page. 13 | Setting `customLogoutPage` to `true` will disable the `/auth/logout` route. This route, if a default provider is set, redirects to the default providers logout page (`/auth//logout`). You have to manually add a logout page to your Nuxt app under `/auth/logout` and use the `logout` method from the `useOidcAuth` composable to logout the user or make sure that you always provide the optional `provider` parameter to the `logout` method. 14 | 15 | ```vue 16 | 19 | 20 | 25 | ``` 26 | 27 | ::callout{icon="i-carbon-warning-alt" color="amber"} 28 | Everything under the `/auth` path is not protected by the global middleware. Make sure to not use this path for any other purpose than authentication. 29 | :: 30 | -------------------------------------------------------------------------------- /docs/content/2.provider/10.zitadel.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Zitadel 3 | description: Zitadel provider documentation 4 | icon: i-carbon-z 5 | --- 6 | 7 | ## Feature/OIDC support 8 | 9 | ✅  PKCE
10 | ✅  Nonce
11 | ✅  State
12 | ❌  Access Token validation
13 | ✅  ID Token validation
14 | 15 | ## Introduction 16 | 17 | For Zitadel you have to provide at least the `baseUrl`, `clientId` and `clientSecret` properties. The `baseUrl` is used to dynamically create the `authorizationUrl`, `tokenUrl`, `logoutUrl` and `userInfoUrl`. 18 | The provider supports PKCE and Code authentication schemes. For PKCE just leave the clientSecret set to an empty string (''). 19 | 20 | ## Provider specific parameters 21 | 22 | This providers doesn't have specific parameters. 23 | 24 | ## Example Configuration 25 | 26 | ::callout{icon="i-carbon-warning-alt" color="amber"} 27 | Never store sensitive values like your client secret in your Nuxt config. Our recommendation is to inject at least client id and client secret via. environment variables. 28 | :: 29 | 30 | ```typescript [nuxt.config.ts] 31 | zitadel: { 32 | clientId: '', 33 | clientSecret: '', // Works with PKCE and Code flow, just leave empty for PKCE 34 | redirectUri: 'http://localhost:3000/auth/zitadel/callback', // Replace with your domain 35 | baseUrl: '', // For example https://PROJECT.REGION.zitadel.cloud 36 | audience: '', // Specify for id token validation, normally same as clientId 37 | logoutRedirectUri: 'https://google.com', // Needs to be registered in Zitadel portal 38 | authenticationScheme: 'none', // Set this to 'header' if Code is used instead of PKCE 39 | }, 40 | ``` 41 | 42 | ### Environment variables 43 | 44 | Dotenv files are only for (local) development. Use a proper configuration management or injection system in production. 45 | 46 | ```ini [.env] 47 | NUXT_OIDC_PROVIDERS_ZITADEL_CLIENT_ID=123456789012345678 48 | NUXT_OIDC_PROVIDERS_ZITADEL_BASE_URL=https://PROJECT.us1.zitadel.cloud/ 49 | ``` 50 | -------------------------------------------------------------------------------- /client/components/ProviderConfigs.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 70 | -------------------------------------------------------------------------------- /docs/app/components/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 65 | -------------------------------------------------------------------------------- /docs/app/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | ui: { 3 | primary: 'green', 4 | gray: 'neutral', 5 | footer: { 6 | bottom: { 7 | left: 'text-sm text-gray-500 dark:text-gray-400', 8 | wrapper: 'border-t border-gray-200 dark:border-gray-800', 9 | }, 10 | }, 11 | }, 12 | seo: { 13 | siteName: 'Nuxt OIDC Auth Documentation', 14 | }, 15 | header: { 16 | logo: { 17 | alt: '~/assets/nuxt-oidc-auth.png', 18 | light: '~/assets/nuxt-oidc-auth.png', 19 | dark: '~/assets/nuxt-oidc-auth-dark.png', 20 | }, 21 | search: true, 22 | colorMode: true, 23 | links: [{ 24 | 'icon': 'i-simple-icons-github', 25 | 'to': 'https://github.com/itpropro/nuxt-oidc-auth', 26 | 'target': '_blank', 27 | 'aria-label': 'Nuxt OIDC Auth Documentation on GitHub', 28 | }], 29 | }, 30 | footer: { 31 | credits: 'Copyright © 2024', 32 | colorMode: false, 33 | links: [{ 34 | 'icon': 'i-simple-icons-nuxtdotjs', 35 | 'to': 'https://nuxt.com', 36 | 'target': '_blank', 37 | 'aria-label': 'Nuxt Website', 38 | }, { 39 | 'icon': 'i-simple-icons-x', 40 | 'to': 'https://x.com/jandamaschke', 41 | 'target': '_blank', 42 | 'aria-label': 'Jan-Henrik Damaschke X', 43 | }, { 44 | 'icon': 'i-simple-icons-github', 45 | 'to': 'https://github.com/itpropro/nuxt-oidc-auth', 46 | 'target': '_blank', 47 | 'aria-label': 'Nuxt OIDC Auth GitHub', 48 | }], 49 | }, 50 | toc: { 51 | title: 'Table of Contents', 52 | bottom: { 53 | title: 'Community', 54 | // edit: 'https://github.com/nuxt-ui-pro/docs/edit/main/content', 55 | links: [{ 56 | icon: 'i-carbon-star', 57 | label: 'Star on GitHub', 58 | to: 'https://github.com/itpropro/nuxt-oidc-auth', 59 | target: '_blank', 60 | }, { 61 | icon: 'i-carbon-block-storage', 62 | label: 'Nuxt modules', 63 | to: 'https://nuxt.com/modules/nuxt-oidc-auth', 64 | target: '_blank', 65 | }], 66 | }, 67 | }, 68 | }) 69 | -------------------------------------------------------------------------------- /test/redirect.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { url } from '@nuxt/test-utils/e2e' 3 | import { expect, test } from '@nuxt/test-utils/playwright' 4 | 5 | test.use({ 6 | // @ts-expect-error Config overwrite 7 | nuxt: { 8 | rootDir: fileURLToPath(new URL('./fixtures/oidcApp', import.meta.url)), 9 | build: false, 10 | buildDir: fileURLToPath(new URL('./fixtures/oidcApp/', import.meta.url)), 11 | nuxtConfig: { 12 | runtimeConfig: { 13 | oidc: { 14 | providers: { 15 | keycloak: { 16 | logoutRedirectUri: 'http://localhost:3000', 17 | clientId: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID, 18 | clientSecret: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET, 19 | }, 20 | }, 21 | }, 22 | }, 23 | }, 24 | }, 25 | }) 26 | 27 | test('redirect on logout', async ({ page, goto }) => { 28 | const provider = 'keycloak' 29 | const providerUrl = url(`/auth/login`) 30 | await goto(providerUrl) 31 | await page.click(`button[name="${provider}"]`) 32 | await page.fill('input[name="username"]', 'testuser') 33 | await page.fill('input[name="password"]', 'p@ssword') 34 | await page.click('input[name="login"]') 35 | await page.waitForURL(url('/')) 36 | await page.click('button[name="logout"]') 37 | expect(page.url()).toMatch(/^http:\/\/localhost:8080/) 38 | }) 39 | 40 | test('session is cleared on clear', async ({ page, goto }) => { 41 | const provider = 'keycloak' 42 | const providerUrl = url(`/auth/login`) 43 | await goto(providerUrl) 44 | await page.click(`button[name="${provider}"]`) 45 | await page.fill('input[name="username"]', 'testuser') 46 | await page.fill('input[name="password"]', 'p@ssword') 47 | await page.click('input[name="login"]') 48 | await page.waitForURL(url('/')) 49 | await page.click('button[name="clear"]') 50 | const loggedIn = await page.locator('div[name="loggedIn"]').textContent() 51 | expect(loggedIn).toBe('false') 52 | await page.click('button[name="fetch"]') 53 | expect(loggedIn).toBe('false') 54 | }) 55 | -------------------------------------------------------------------------------- /playground/composables/useProviders.ts: -------------------------------------------------------------------------------- 1 | // @unocss-include 2 | 3 | interface Provider { 4 | label: string 5 | name: string 6 | disabled: boolean 7 | icon: string 8 | } 9 | 10 | export function useProviders(currentProvider: string) { 11 | const providers = ref([ 12 | { 13 | label: 'Auth0', 14 | name: 'auth0', 15 | disabled: Boolean(currentProvider === 'auth0'), 16 | icon: 'i-simple-icons-auth0', 17 | }, 18 | { 19 | label: 'AWS Cognito', 20 | name: 'cognito', 21 | disabled: Boolean(currentProvider === 'cognito'), 22 | icon: 'i-simple-icons-amazoncognito', 23 | }, 24 | { 25 | label: 'GitHub', 26 | name: 'github', 27 | disabled: Boolean(currentProvider === 'github'), 28 | icon: 'i-simple-icons-github', 29 | }, 30 | { 31 | label: 'Keycloak', 32 | name: 'keycloak', 33 | disabled: Boolean(currentProvider === 'keycloak'), 34 | icon: 'i-simple-icons-cncf', 35 | }, 36 | { 37 | label: 'Logto', 38 | name: 'logto', 39 | disabled: Boolean(currentProvider === 'logto'), 40 | icon: 'i-simple-icons-openid', 41 | }, 42 | { 43 | label: 'PayPal', 44 | name: 'paypal', 45 | disabled: Boolean(currentProvider === 'paypal'), 46 | icon: 'i-simple-icons-paypal', 47 | }, 48 | { 49 | label: 'Microsoft', 50 | name: 'microsoft', 51 | disabled: Boolean(currentProvider === 'entra'), 52 | icon: 'i-simple-icons-microsoft', 53 | }, 54 | { 55 | label: 'Microsoft Entra ID', 56 | name: 'entra', 57 | disabled: Boolean(currentProvider === 'entra'), 58 | icon: 'i-simple-icons-microsoftazure', 59 | }, 60 | { 61 | label: 'Zitadel', 62 | name: 'zitadel', 63 | disabled: Boolean(currentProvider === 'zitadel'), 64 | icon: 'i-majesticons-puzzle', 65 | }, 66 | { 67 | label: 'Generic OIDC', 68 | name: 'oidc', 69 | disabled: Boolean(currentProvider === 'oidc'), 70 | icon: 'i-simple-icons-openid', 71 | }, 72 | ]) 73 | return { 74 | providers, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/2.installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: Get started with nuxt-oidc-auth 4 | --- 5 | 6 | ## Quick Start 7 | 8 | ### Add `nuxt-oidc-auth` dependency to your project 9 | 10 | Using nuxi 11 | 12 | ::code-group 13 | ```bash [pnpm] 14 | pnpm dlx nuxi@latest module add nuxt-oidc-auth 15 | ``` 16 | 17 | ```bash [yarn] 18 | yarn dlx nuxi@latest module add nuxt-oidc-auth 19 | ``` 20 | 21 | ```bash [npm] 22 | npx nuxi@latest module add nuxt-oidc-auth 23 | ``` 24 | :: 25 | 26 | Or manually installing 27 | 28 | ::code-group 29 | 30 | ```bash [pnpm] 31 | pnpm add nuxt-oidc-auth 32 | ``` 33 | 34 | ```bash [yarn] 35 | yarn add nuxt-oidc-auth 36 | ``` 37 | 38 | ```bash [npm] 39 | npm install nuxt-oidc-auth 40 | ``` 41 | :: 42 | 43 | ### Configure provider 44 | 45 | Configure the required properties for one of the [predefined providers](/provider) in your nuxt.config.ts under `oidc.providers` and save the provider secrets in your `.env file`. 46 | 47 | Example for [GitHub](/provider/github): 48 | 49 | ```typescript [nuxt.config.ts] 50 | github: { 51 | redirectUri: 'http://localhost:3000/auth/github/callback', 52 | clientId: '', 53 | clientSecret: '', 54 | filterUserInfo: ['login', 'id', 'avatar_url', 'name', 'email'], 55 | }, 56 | ``` 57 | 58 | ```ini [.env] 59 | NUXT_OIDC_PROVIDERS_GITHUB_CLIENT_SECRET=CLIENT_SECRET 60 | NUXT_OIDC_PROVIDERS_GITHUB_CLIENT_ID=CLIENT_ID 61 | ``` 62 | 63 | You should also consider setting the [nuxt-oidc-auth secrets](/getting-started/security#configure-secrets) to persist session data across restarts (every code change in local development). 64 | 65 | ```ini [.env] 66 | # .env 67 | NUXT_OIDC_TOKEN_KEY=base64_encoded_key 68 | NUXT_OIDC_SESSION_SECRET=48_characters_random_string 69 | NUXT_OIDC_AUTH_SESSION_SECRET=48_characters_random_string 70 | ``` 71 | 72 | That's it! You can now add authentication with a predifined provider or a custom OIDC provider to your Nuxt app ✨ 73 | 74 | ::u-button 75 | --- 76 | class: mr-4 77 | icon: i-simple-icons-stackblitz 78 | label: Play on StackBlitz 79 | target: _blank 80 | to: https://stackblitz.com/github/itpropro/nuxt-oidc-auth/tree/main/playground 81 | --- 82 | :: 83 | -------------------------------------------------------------------------------- /docs/content/5.dev-mode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dev mode 3 | description: Development mode for seamless local development 4 | --- 5 | 6 | ## Dev mode 7 | 8 | Since 0.10.0, there is a local dev mode available. It can only be enabled if the `NODE_ENV` environment variable is not set to `prod/production` AND dev mode is explicitly enabled in the config. The dev mode is for ***local*** and ***offline*** development and returns a static user object that can be configured in the config or by variables in `.env`. 9 | The following fields in the returned [user object](/composable#user-object) can be configured: 10 | 11 | - `claims`: `devMode.claims` setting 12 | - `provider`: `devMode.provider` setting 13 | - `userName`: `devMode.userName` setting 14 | - `userInfo`: `devMode.userInfo` setting 15 | - `idToken`: `devMode.idToken` setting 16 | - `accessToken`: `devMode.accessToken` setting 17 | 18 | Please refer to [user object](/composable#user-object) for required types. 19 | 20 | ### Enabling 21 | 22 | To enable the dev mode, you have to make sure at least the following settings are set: 23 | 24 | - `session` -> `expirationCheck` needs to be turned off (`false`) 25 | - `devMode` -> `enabled` set to `true` in the `oidc` part of your `nuxt.config.ts` 26 | 27 | ### Token generation 28 | 29 | If needed, the dev mode can generate a valid signed access token if the settting `devMode` -> `generateAccessToken` is set to `true`. This token will be exposed in the `user.accessToken` property. 30 | The properties on the generated token are 31 | 32 | - `iat` (issued at): current DateTime, 33 | - `iss` (issuer): `devMode.issuer` setting, default `nuxt:oidc:auth:issuer` 34 | - `aud`: `devMode.audience` setting, default `nuxt:oidc:auth:audience` 35 | - `sub`: `devMode.subject` setting, default `nuxt:oidc:auth:subject` 36 | - `exp`: current DateTime + 24h 37 | 38 | ::callout{icon="i-carbon-warning-alt" color="amber"} 39 | The access token will be generated with a fixed local secret and can in no way be considered secure. Dev mode can only be enabled in local development and should exclusively be used there for testing purposes. Never set any environment variables on your production systems that could put any component into development mode. 40 | :: 41 | -------------------------------------------------------------------------------- /src/runtime/providers/keycloak.ts: -------------------------------------------------------------------------------- 1 | import { generateProviderUrl } from '../server/utils/config' 2 | import { createProviderFetch, defineOidcProvider } from '../server/utils/provider' 3 | 4 | type KeycloakRequiredFields = 'baseUrl' | 'clientId' | 'clientSecret' | 'redirectUri' 5 | 6 | interface KeycloakProviderConfig { 7 | /** 8 | * This parameter allows to slightly customize the login flow on the Keycloak server side. For example, enforce displaying the login screen in case of value login. 9 | * @default undefined 10 | */ 11 | prompt?: string 12 | /** 13 | * Used to pre-fill the username/email field on the login form. 14 | * @default undefined 15 | */ 16 | loginHint?: string 17 | /** 18 | * Used to tell Keycloak to skip showing the login page and automatically redirect to the specified identity provider instead. 19 | * @default undefined 20 | */ 21 | idpHint?: string 22 | /** 23 | * Sets the 'ui_locales' query param. 24 | * @default undefined 25 | */ 26 | locale?: string 27 | } 28 | 29 | export const keycloak = defineOidcProvider({ 30 | authorizationUrl: 'protocol/openid-connect/auth', 31 | tokenUrl: 'protocol/openid-connect/token', 32 | userInfoUrl: 'protocol/openid-connect/userinfo', 33 | tokenRequestType: 'form-urlencoded', 34 | userNameClaim: 'preferred_username', 35 | pkce: true, 36 | state: false, 37 | nonce: true, 38 | requiredProperties: [ 39 | 'clientId', 40 | 'clientSecret', 41 | 'authorizationUrl', 42 | 'tokenUrl', 43 | 'redirectUri', 44 | ], 45 | additionalLogoutParameters: { 46 | idTokenHint: '', 47 | }, 48 | sessionConfiguration: { 49 | expirationCheck: true, 50 | automaticRefresh: true, 51 | expirationThreshold: 240, 52 | }, 53 | validateAccessToken: true, 54 | validateIdToken: false, 55 | exposeIdToken: true, 56 | baseUrl: '', 57 | logoutUrl: 'protocol/openid-connect/logout', 58 | logoutRedirectParameterName: 'post_logout_redirect_uri', 59 | async openIdConfiguration(config: any) { 60 | const configUrl = generateProviderUrl(config.baseUrl, '.well-known/openid-configuration') 61 | const customFetch = await createProviderFetch(config) 62 | return await customFetch(configUrl) 63 | }, 64 | }) 65 | -------------------------------------------------------------------------------- /docs/app/components/content/contentImage.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 66 | -------------------------------------------------------------------------------- /src/runtime/providers/logto.ts: -------------------------------------------------------------------------------- 1 | import { normalizeURL, withHttps, withoutTrailingSlash } from 'ufo' 2 | import { createProviderFetch, defineOidcProvider } from '../server/utils/provider' 3 | 4 | interface LogtoProviderConfig { 5 | /** 6 | * Specifies the first screen that the user will see. 7 | * @default undefined 8 | */ 9 | firstScreen?: string 10 | /** 11 | * Specifies the identifier types that the sign-in or sign-up form will accept. 12 | * @default undefined 13 | */ 14 | identifier?: string 15 | /** 16 | * Populates the identifier field with the user's email address or username. (This is a OIDC standard parameter) 17 | * @default undefined 18 | */ 19 | loginHint?: string 20 | /** 21 | * Indicates the type of user interaction that is required. Valid values are login, none, consent, and select_account. 22 | * @default undefined 23 | */ 24 | prompt?: 'login' | 'none' | 'consent' | 'select_account' 25 | } 26 | 27 | type LogtoRequiredFields = 'baseUrl' | 'clientId' | 'clientSecret' 28 | 29 | export const logto = defineOidcProvider({ 30 | logoutRedirectParameterName: 'post_logout_redirect_uri', 31 | tokenRequestType: 'form-urlencoded', 32 | authenticationScheme: 'body', 33 | userInfoUrl: 'oidc/me', 34 | pkce: true, 35 | state: true, 36 | nonce: true, 37 | scopeInTokenRequest: false, 38 | userNameClaim: '', 39 | authorizationUrl: '/oidc/auth', 40 | tokenUrl: '/oidc/token', 41 | logoutUrl: '/oidc/session/end', 42 | scope: ['profile', 'openid', 'offline_access'], 43 | requiredProperties: [ 44 | 'baseUrl', 45 | 'clientId', 46 | 'clientSecret', 47 | 'authorizationUrl', 48 | 'tokenUrl', 49 | ], 50 | // For offline_access, we set prompt to 'consent' 51 | additionalAuthParameters: { 52 | prompt: 'consent', 53 | }, 54 | additionalLogoutParameters: { 55 | idTokenHint: '', 56 | }, 57 | async openIdConfiguration(config: any) { 58 | const baseUrl = normalizeURL(withoutTrailingSlash(withHttps(config.baseUrl as string))) 59 | const customFetch = await createProviderFetch(config) 60 | return await customFetch(`${baseUrl}/oidc/.well-known/openid-configuration`) 61 | }, 62 | skipAccessTokenParsing: true, 63 | validateAccessToken: false, 64 | validateIdToken: true, 65 | exposeIdToken: true, 66 | }) 67 | -------------------------------------------------------------------------------- /docs/content/2.provider/3.aws-cognito.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: AWS Cognito 3 | description: AWS Cognito provider documentation 4 | icon: i-simple-icons-amazonaws 5 | --- 6 | 7 | ## Feature/OIDC support 8 | 9 | ✅  PKCE
10 | ✅  Nonce
11 | ✅  State
12 | ❌  Access Token validation
13 | ❌  ID Token validation
14 | 15 | AWS Congito doesn't correctly implement the OAuth 2 standard and doesn't provide a `aud` field for the audience. Therefore it is not possible to verify the access or id token. 16 | 17 | ## Introduction 18 | 19 | For AWS Cognito you have to provide at least the `baseUrl`, `clientId`, `clientSecret` and `logoutRedirectUri` properties. The `baseUrl` is used to dynamically create the `authorizationUrl`, `tokenUrl`, `logoutUrl` and `userInfoUrl`. 20 | The only supported OAuth grant type is `Authorization code grant`. 21 | The final url should look something like this `https://cognito-idp.eu-north-1.amazonaws.com/eu-north-1_SOMEID/.well-known/openid-configuration`. 22 | You will also encounter an error, if you have not correctly registered the `redirectUri` under "Allowed callback URLs" or the `logoutRedirectUri` under "Allowed sign-out URLs". 23 | If you need additional scopes, specify them in the `scope` property in you nuxt config like `scope: ['openid', 'email', 'profile'],`. 24 | 25 | ## Example Configuration 26 | 27 | ::callout{icon="i-carbon-warning-alt" color="amber"} 28 | Never store sensitive values like your client secret in your Nuxt config. Our recommendation is to inject at least client id and client secret via. environment variables. 29 | :: 30 | 31 | ```typescript [nuxt.config.ts] 32 | cognito: { 33 | clientId: '', 34 | redirectUri: 'http://localhost:3000/auth/cognito/callback', 35 | clientSecret: '', 36 | scope: ['openid', 'email', 'profile'], 37 | logoutRedirectUri: 'https://google.com', 38 | baseUrl: '', 39 | exposeIdToken: true, // This is necessary to validate the logout redirect. If you don't need the ID token and don't use a logout redirect, set this to false. 40 | }, 41 | ``` 42 | 43 | ### Environment variables 44 | 45 | Dotenv files are only for (local) development. Use a proper configuration management or injection system in production. 46 | 47 | ```ini [.env] 48 | NUXT_OIDC_PROVIDERS_COGNITO_CLIENT_ID=CLIENT_ID 49 | NUXT_OIDC_PROVIDERS_COGNITO_CLIENT_SECRET=CLIENT_SECRET 50 | NUXT_OIDC_PROVIDERS_COGNITO_BASE_URL=https://YOURAPP.auth.eu-north-1.amazoncognito.com 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/content/2.provider/99.oidc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Generic OIDC (advanced) 3 | description: Generic OIDC provider documentation 4 | icon: i-simple-icons-openid 5 | --- 6 | 7 | ## Introduction 8 | 9 | This is a generic OIDC provider that doesn't provide any preconfiguration or helpers to automatically generate URL. 10 | If your provider is not listed, you should be able to configure it with this provider. 11 | 12 | This is the generic providers default configuration: 13 | 14 | ```typescript 15 | const defaults: Partial = { 16 | clientId: '', 17 | redirectUri: '', 18 | clientSecret: '', 19 | authorizationUrl: '', 20 | tokenUrl: '', 21 | responseType: 'code', 22 | authenticationScheme: 'header', 23 | grantType: 'authorization_code', 24 | pkce: false, 25 | state: true, 26 | nonce: false, 27 | scope: ['openid'], 28 | scopeInTokenRequest: false, 29 | tokenRequestType: 'form', 30 | requiredProperties: [ 31 | 'clientId', 32 | 'redirectUri', 33 | 'clientSecret', 34 | 'authorizationUrl', 35 | 'tokenUrl', 36 | ], 37 | validateAccessToken: true, 38 | validateIdToken: true, 39 | skipAccessTokenParsing: false, 40 | exposeAccessToken: false, 41 | exposeIdToken: false, 42 | callbackRedirectUrl: '/', 43 | allowedClientAuthParameters: undefined, 44 | logoutUrl: '', 45 | sessionConfiguration: undefined, 46 | additionalAuthParameters: undefined, 47 | additionalTokenParameters: undefined, 48 | additionalLogoutParameters: undefined, 49 | excludeOfflineScopeFromTokenRequest: false, 50 | } 51 | ``` 52 | 53 | ## Example Configuration 54 | 55 | ::callout{icon="i-carbon-warning-alt" color="amber"} 56 | Never store sensitive values like your client secret in your Nuxt config. Our recommendation is to inject at least client id and client secret via. environment variables. 57 | :: 58 | 59 | ```typescript [nuxt.config.ts] 60 | oidc: { 61 | clientId: '', 62 | clientSecret: '', 63 | responseType: 'code id_token', 64 | validateAccessToken: false, 65 | validateIdToken: false, 66 | skipAccessTokenParsing: true, 67 | state: true, 68 | nonce: true, 69 | pkce: true, 70 | tokenRequestType: 'form-urlencoded', 71 | scope: ['openid', 'email'], 72 | authorizationUrl: '', 73 | tokenUrl: '', 74 | userInfoUrl: '', 75 | redirectUri: '', 76 | }, 77 | ``` 78 | 79 | ### Environment variables 80 | 81 | Dotenv files are only for (local) development. Use a proper configuration management or injection system in production. 82 | 83 | ```ini [.env] 84 | EVERY AVAILABLE PARAMETER 85 | ``` 86 | -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | 3 | export default defineNuxtConfig({ 4 | 5 | extends: ['@nuxt/ui-pro'], 6 | 7 | modules: [ 8 | '@nuxt/fonts', 9 | '@nuxt/content', 10 | '@vueuse/nuxt', 11 | '@nuxt/scripts', 12 | '@nuxt/ui', 13 | '@nuxtjs/seo', 14 | '@nuxt/image', 15 | 'nuxt-vitalizer', 16 | ], 17 | 18 | $production: { 19 | scripts: { 20 | registry: { 21 | plausibleAnalytics: { 22 | domain: 'nuxtoidc.cloud', 23 | }, 24 | }, 25 | }, 26 | }, 27 | ssr: true, 28 | 29 | devtools: { 30 | enabled: true, 31 | 32 | timeline: { 33 | enabled: true, 34 | }, 35 | }, 36 | 37 | app: { 38 | head: { 39 | seoMeta: { 40 | themeColor: [ 41 | { content: '#18181b', media: '(prefers-color-scheme: dark)' }, 42 | { content: 'white', media: '(prefers-color-scheme: light)' }, 43 | ], 44 | }, 45 | templateParams: { 46 | separator: '·', 47 | }, 48 | }, 49 | }, 50 | 51 | site: { 52 | name: 'Nuxt OIDC Auth Docs', 53 | url: 'nuxtoidc.cloud', 54 | }, 55 | 56 | routeRules: { 57 | '/': { prerender: true }, 58 | '/api/search.json': { prerender: true }, 59 | '/sitemap.xml': { prerender: true }, 60 | }, 61 | 62 | future: { 63 | compatibilityVersion: 4, 64 | }, 65 | 66 | compatibilityDate: '2024-07-03', 67 | 68 | nitro: { 69 | prerender: { 70 | crawlLinks: true, 71 | routes: ['/'], 72 | failOnError: false, 73 | }, 74 | preset: 'azure', 75 | }, 76 | 77 | hooks: { 78 | 'components:extend': (components) => { 79 | const globals = components.filter(c => ['UButton', 'UIcon', 'UAlert'].includes(c.pascalName)) 80 | globals.forEach(c => c.global = true) 81 | }, 82 | }, 83 | 84 | fonts: { 85 | families: [ 86 | { name: 'DM Sans', provider: 'bunny', weights: [400, 700] }, 87 | ], 88 | providers: { 89 | google: false, 90 | }, 91 | }, 92 | 93 | icon: { 94 | collections: ['simple-icons', 'carbon', 'heroicons', 'vscode-icons'], 95 | clientBundle: { 96 | scan: true, 97 | }, 98 | serverBundle: { 99 | collections: ['simple-icons', 'carbon', 'heroicons', 'vscode-icons'], 100 | }, 101 | }, 102 | 103 | ogImage: { 104 | zeroRuntime: true, 105 | }, 106 | 107 | sitemap: { 108 | strictNuxtContentPaths: true, 109 | }, 110 | }) 111 | -------------------------------------------------------------------------------- /src/devtools.ts: -------------------------------------------------------------------------------- 1 | import type { Resolver } from '@nuxt/kit' 2 | import type { Nuxt } from 'nuxt/schema' 3 | import { existsSync } from 'node:fs' 4 | import { extendServerRpc, onDevToolsInitialized } from '@nuxt/devtools-kit' 5 | 6 | const DEVTOOLS_UI_ROUTE = '/__nuxt-oidc-auth' 7 | const DEVTOOLS_UI_LOCAL_PORT = 3300 8 | const RPC_NAMESPACE = 'nuxt-oidc-auth-rpc' 9 | 10 | interface ServerFunctions { 11 | getNuxtOidcAuthSecrets: () => Record<'tokenKey' | 'sessionSecret' | 'authSessionSecret', string> 12 | } 13 | 14 | interface ClientFunctions {} 15 | 16 | export function setupDevToolsUI(nuxt: Nuxt, resolver: Resolver) { 17 | const clientPath = resolver.resolve('./client') 18 | const isProductionBuild = existsSync(clientPath) 19 | 20 | // Serve production-built client (used when package is published) 21 | if (isProductionBuild) { 22 | nuxt.hook('vite:serverCreated', async (server) => { 23 | const sirv = await import('sirv').then(r => r.default || r) 24 | server.middlewares.use( 25 | DEVTOOLS_UI_ROUTE, 26 | sirv(clientPath, { dev: true, single: true }), 27 | ) 28 | }) 29 | } 30 | // In local development, start a separate Nuxt Server and proxy to serve the client 31 | else { 32 | nuxt.hook('vite:extendConfig', (config) => { 33 | config.server = config.server || {} 34 | config.server.proxy = config.server.proxy || {} 35 | config.server.proxy[DEVTOOLS_UI_ROUTE] = { 36 | target: `http://localhost:${DEVTOOLS_UI_LOCAL_PORT}${DEVTOOLS_UI_ROUTE}`, 37 | changeOrigin: true, 38 | followRedirects: true, 39 | rewrite: path => path.replace(DEVTOOLS_UI_ROUTE, ''), 40 | } 41 | }) 42 | } 43 | 44 | // Wait for DevTools to be initialized 45 | onDevToolsInitialized(async () => { 46 | extendServerRpc(RPC_NAMESPACE, { 47 | getNuxtOidcAuthSecrets() { 48 | const tokenKey = process.env.NUXT_OIDC_TOKEN_KEY || '' 49 | const sessionSecret = process.env.NUXT_OIDC_SESSION_SECRET || '' 50 | const authSessionSecret = process.env.NUXT_OIDC_AUTH_SESSION_SECRET || '' 51 | return { 52 | tokenKey, 53 | sessionSecret, 54 | authSessionSecret, 55 | } 56 | }, 57 | }) 58 | }) 59 | 60 | nuxt.hook('devtools:customTabs', (tabs) => { 61 | tabs.push({ 62 | name: 'nuxt-oidc-auth', 63 | title: 'Nuxt OIDC Auth', 64 | icon: 'carbon:rule-locked', 65 | view: { 66 | type: 'iframe', 67 | src: DEVTOOLS_UI_ROUTE, 68 | }, 69 | }) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /src/runtime/providers/auth0.ts: -------------------------------------------------------------------------------- 1 | import { normalizeURL, withHttps, withoutTrailingSlash } from 'ufo' 2 | import { createProviderFetch, defineOidcProvider } from '../server/utils/provider' 3 | 4 | interface Auth0ProviderConfig { 5 | /** 6 | * Forces the user to sign in with a specific connection. For example, you can pass a value of github to send the user directly to GitHub to log in with their GitHub account. When not specified, the user sees the Auth0 Lock screen with all configured connections. You can see a list of your configured connections on the Connections tab of your application. 7 | * @default undefined 8 | */ 9 | connection?: string 10 | /** 11 | * ID of the organization to use when authenticating a user. When not provided, if your application is configured to Display Organization Prompt, the user will be able to enter the organization name when authenticating. 12 | * @default undefined 13 | */ 14 | organization?: string 15 | /** 16 | * Ticket ID of the organization invitation. When inviting a member to an Organization, your application should handle invitation acceptance by forwarding the invitation and organization key-value pairs when the user accepts the invitation. 17 | * @default undefined 18 | */ 19 | invitation?: string 20 | /** 21 | * Populates the username/email field for the login or signup page when redirecting to Auth0. Supported by the Universal Login experience. 22 | * @default undefined 23 | */ 24 | loginHint?: string 25 | /** 26 | * The unique identifier of the API your web app wants to access. 27 | * @default undefined 28 | */ 29 | audience?: string 30 | } 31 | 32 | type Auth0RequiredFields = 'baseUrl' | 'clientId' | 'clientSecret' 33 | 34 | export const auth0 = defineOidcProvider({ 35 | tokenRequestType: 'json', 36 | authenticationScheme: 'body', 37 | userInfoUrl: 'userinfo', 38 | pkce: true, 39 | state: true, 40 | nonce: false, 41 | scopeInTokenRequest: false, 42 | userNameClaim: '', 43 | authorizationUrl: 'authorize', 44 | tokenUrl: 'oauth/token', 45 | logoutUrl: '', 46 | requiredProperties: [ 47 | 'baseUrl', 48 | 'clientId', 49 | 'clientSecret', 50 | 'authorizationUrl', 51 | 'tokenUrl', 52 | ], 53 | async openIdConfiguration(config: any) { 54 | const baseUrl = normalizeURL(withoutTrailingSlash(withHttps(config.baseUrl as string))) 55 | const customFetch = await createProviderFetch(config) 56 | return await customFetch(`${baseUrl}/.well-known/openid-configuration`) 57 | }, 58 | validateAccessToken: true, 59 | validateIdToken: false, 60 | }) 61 | -------------------------------------------------------------------------------- /client/components/Secrets.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 81 | -------------------------------------------------------------------------------- /docs/app/pages/[...slug].vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 95 | -------------------------------------------------------------------------------- /docs/content/2.provider/1.index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | description: Use one of the built in providers for the best experience 4 | --- 5 | 6 | ## Authentication Provider Presets 7 | 8 | Nuxt OIDC Auth includes presets for the following providers with tested default values: 9 | 10 | - [Auth0](/provider/auth0) 11 | - [AWS Cognito](/provider/aws-cognito) 12 | - [Entra ID/Microsoft](/provider/entra) 13 | - [GitHub](/provider/github) 14 | - [KeyCloak](/provider/keycloak) 15 | - [PayPal](/provider/paypal) 16 | - [Zitadel](/provider/zitadel) 17 | - [Generic OIDC](/provider/oidc) 18 | 19 | ### Provider specific configurations 20 | 21 | Some providers have specific additional fields that can be used to extend the authorization, logout or token request. These fields are available via. `additionalAuthParameters`, `additionalLogoutParameters` or `additionalTokenParameters` in the provider configuration. 22 | 23 | ::callout{icon="i-carbon-warning-alt" color="amber"} 24 | Tokens will only be validated if the `clientId` or the optional `audience` field is part of the access_tokens (or id_token if existent) audiences. Even if `validateAccessToken` or `validateIdToken` is set, if the audience doesn't match, the token should not and will not be validated. Some providers like Entra or Zitadel don't or just in certain cases provide a parsable JWT access token. Validation will fail for these and should be disable, even if the audience is set. 25 | :: 26 | 27 | The `redirectUri` property is always required and should always point to the callback uri of the specific provider. For Auth0 it should look like this `https://YOURDOMAIN/auth/auth0/callback`. The playgrounds nuxt.config.ts has examples for multiple providers. 28 | 29 | If there is no preset for your provider, you can add a generic OpenID Connect provider by using the `oidc` provider key in the configuration. Remember to set the required fields and expect your provider to behave slightly different than defined in the OAuth and OIDC specifications. 30 | For security reasons, you should avoid writing the client secret directly in the `nuxt.config.ts` file. You can use environment variables to inject settings into the runtime config. Check the `.env.example` file in the playground folder for reference. 31 | 32 | Also consider creating an issue to request additional providers being added. 33 | 34 | ```ini 35 | # OIDC MODULE CONFIG 36 | NUXT_OIDC_TOKEN_KEY= 37 | NUXT_OIDC_SESSION_SECRET= 38 | NUXT_OIDC_AUTH_SESSION_SECRET= 39 | # AUTH0 PROVIDER CONFIG 40 | NUXT_OIDC_PROVIDERS_AUTH0_CLIENT_SECRET= 41 | NUXT_OIDC_PROVIDERS_AUTH0_CLIENT_ID= 42 | NUXT_OIDC_PROVIDERS_AUTH0_BASE_URL= 43 | # KEYCLOAK PROVIDER CONFIG 44 | NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET= 45 | NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID= 46 | NUXT_OIDC_PROVIDERS_KEYCLOAK_BASE_URL= 47 | ... 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/content/5.single-sign-out.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Single sign-out 3 | description: Cross tab and browser sign out and server side session invalidation 4 | --- 5 | 6 | ## Single sign-out 7 | 8 | Single sign-out is a feature that allows users to sign out of the application in one tab/browser and have the same user session invalidated in other tabs/browsers. 9 | 10 | This is useful for compliance purposes, as it ensures that if a user signs out of the application, the session will be invalidated across all devices. 11 | 12 | ::callout{icon="i-carbon-warning-alt" color="amber"} 13 | There are still cases where the session is still usable, as the user can theoretically block the connection to the server endpoint. The server side session will still be invalidated, but the session data will stay in the browser until the session expires or the user fetches or refreshes the session. 14 | :: 15 | 16 | ## How it works 17 | 18 | Single sign-out registers a plugin in the client that subscribes to a server sent event endpoint on the server. This endpoint is used to send a message to the client when the user signs out in a different tab/browser. 19 | 20 | The feature uses a unique session ID to track related sessions across tabs/browsers. When a user signs out: 21 | 22 | 1. The user logs out 23 | 2. The server invalidates the session 24 | 3. A message is broadcast to all connected clients with the same session ID 25 | 4. Clients receiving the message log out as well 26 | 5. When a user reconnects with an old session, the plugin will automatically invalidate sessions that may still be active in the browser 27 | 28 | If there is no refresh token, meaning there is no persistent session, single sign-out will only work in the same browser and with the same session. 29 | 30 | ## Configuration 31 | 32 | Example configuration in `nuxt.config.ts` with Keycloak provider: 33 | 34 | ```ts [nuxt.config.ts] 35 | keycloak: { 36 | baseUrl: '', 37 | clientId: '', 38 | clientSecret: '', 39 | redirectUri: 'http://localhost:3000/auth/keycloak/callback', 40 | userNameClaim: 'preferred_username', 41 | logoutRedirectUri: 'http://localhost:3000', 42 | // Single sign-out 43 | sessionConfiguration: { 44 | singleSignOut: true, 45 | singleSignOutIdField: 'sub', 46 | }, 47 | }, 48 | ``` 49 | 50 | ### Options 51 | 52 | | Option | Type | Default | Description | 53 | |--------|------|---------|-------------| 54 | | `singleSignOut` | `boolean` | `false` | Enable single sign-out across tabs | 55 | | `singleSignOutIdField` | `'sub' \| 'aud'` | `'sub'` | Token field for session ID | 56 | 57 | ## Provider Support 58 | 59 | Single sign-out is supported by all providers that issue refresh tokens. The feature requires a persistent session to work across browsers. 60 | -------------------------------------------------------------------------------- /src/runtime/server/utils/config.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderConfigs, ProviderKeys } from '../../types' 2 | import type { OidcProviderConfig } from './provider' 3 | import { snakeCase } from 'scule' 4 | import { cleanDoubleSlashes, joinURL, parseURL, withHttps, withoutTrailingSlash } from 'ufo' 5 | 6 | export interface ValidationResult { 7 | valid: boolean 8 | missingProperties?: string[] 9 | config: T 10 | } 11 | 12 | /** 13 | * Validate a configuration object 14 | * @param config The configuration object to validate 15 | * @returns ValidationResult object with the validation result and the validated config stripped of optional properties 16 | */ 17 | export function validateConfig(config: T, requiredProps: string[]): ValidationResult { 18 | const missingProperties: string[] = [] 19 | let valid = true 20 | for (const prop of requiredProps) { 21 | if (!(prop in (config as object))) { 22 | valid = false 23 | missingProperties.push(prop.toString()) 24 | } 25 | } 26 | return { valid, missingProperties, config } 27 | } 28 | 29 | export function generateProviderUrl(baseUrl: string, relativeUrl?: string) { 30 | const parsedUrl = parseURL(baseUrl) 31 | return parsedUrl.protocol ? withoutTrailingSlash(cleanDoubleSlashes(joinURL(baseUrl, '/', relativeUrl || ''))) : withoutTrailingSlash(cleanDoubleSlashes(withHttps(joinURL(baseUrl, '/', relativeUrl || '')))) 32 | } 33 | 34 | export function replaceInjectedParameters( 35 | injectedParameters: Array, 36 | providerOptions: OidcProviderConfig, 37 | providerPreset: ProviderConfigs[keyof ProviderConfigs], 38 | provider: ProviderKeys, 39 | ): void { 40 | const additionalParameterKeys = ['additionalAuthParameters', 'additionalTokenParameters', 'additionalLogoutParameters'] as Array> 41 | additionalParameterKeys.forEach((parameterKey) => { 42 | const presetParams = providerPreset[parameterKey] 43 | if (!presetParams) 44 | return 45 | const providerParams = providerOptions[parameterKey] 46 | if (!providerParams) { 47 | providerOptions[parameterKey] = {} 48 | } 49 | Object.entries(presetParams).forEach(([key, value]) => { 50 | injectedParameters.forEach((injectedParameter) => { 51 | const placeholder = `{${injectedParameter}}` 52 | if ((value as string).includes(placeholder)) { 53 | providerOptions[parameterKey]![key] = (value as string).replace( 54 | placeholder, 55 | providerOptions[injectedParameter] as string || process.env[`NUXT_OIDC_PROVIDERS_${provider.toUpperCase()}_${snakeCase(injectedParameter).toUpperCase()}`] || '', 56 | ) 57 | } 58 | }) 59 | }) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-oidc-auth", 3 | "type": "module", 4 | "version": "1.0.0-beta.5", 5 | "private": false, 6 | "packageManager": "pnpm@9.15.4", 7 | "description": "OIDC authentication module for Nuxt SSR", 8 | "license": "MIT", 9 | "homepage": "https://github.com/itpropro/nuxt-oidc-auth#readme", 10 | "repository": "itpropro/nuxt-oidc-auth", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/types.d.ts", 14 | "import": "./dist/module.mjs", 15 | "require": "./dist/module.cjs" 16 | }, 17 | "./package.json": "./package.json", 18 | "./runtime/*": "./dist/runtime/*" 19 | }, 20 | "main": "./dist/module.cjs", 21 | "types": "./dist/types.d.ts", 22 | "files": [ 23 | "dist" 24 | ], 25 | "scripts": { 26 | "prepack": "nuxt-module-build build && pnpm client:build", 27 | "client:build": "nuxi generate client", 28 | "client:dev": "nuxi dev client --port 3300", 29 | "dev:client": "concurrently \"nuxi dev client --port 3300\" \"nuxi dev playground\"", 30 | "dev": "nuxi dev playground", 31 | "dev:docs": "nuxi dev docs", 32 | "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare client", 33 | "release": "pnpm lint && pnpm prepack && changelogen --release && git push --follow-tags && pnpm publish --access=public", 34 | "lint": "eslint . && tsc --noemit", 35 | "test": "vitest run", 36 | "test:watch": "vitest watch" 37 | }, 38 | "peerDependencies": { 39 | "undici": "^7.2.1" 40 | }, 41 | "peerDependenciesMeta": { 42 | "undici": { 43 | "optional": true 44 | } 45 | }, 46 | "dependencies": { 47 | "@nuxt/devtools-kit": "^1.7.0", 48 | "@nuxt/devtools-ui-kit": "^1.7.0", 49 | "consola": "^3.4.0", 50 | "defu": "^6.1.4", 51 | "h3": "^1.13.1", 52 | "jose": "^5.9.6", 53 | "ofetch": "^1.4.1", 54 | "scule": "^1.3.0", 55 | "sirv": "^3.0.0", 56 | "ufo": "^1.5.4", 57 | "uncrypto": "^0.1.3", 58 | "undici": "^7.2.3", 59 | "undio": "^0.2.0" 60 | }, 61 | "devDependencies": { 62 | "@antfu/eslint-config": "^3.14.0", 63 | "@nuxt/devtools": "^1.7.0", 64 | "@nuxt/eslint-config": "^0.7.5", 65 | "@nuxt/kit": "^3.15.2", 66 | "@nuxt/module-builder": "^0.8.4", 67 | "@nuxt/schema": "^3.15.2", 68 | "@nuxt/test-utils": "^3.15.4", 69 | "@playwright/test": "^1.49.1", 70 | "@types/node": "^22.10.7", 71 | "@unocss/eslint-plugin": "^65.4.2", 72 | "changelogen": "^0.5.7", 73 | "concurrently": "^9.1.2", 74 | "eslint": "^9.18.0", 75 | "nuxt": "^3.15.2", 76 | "typescript": "5.6.3", 77 | "vitest": "^3.0.2", 78 | "vue-tsc": "^2.2.0" 79 | }, 80 | "pnpm": { 81 | "overrides": { 82 | "sharp": "0.33.5", 83 | "typescript": "5.6.3" 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /docs/app/assets/nuxt-oidc-auth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/staticwebapp.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "route": "/provider/microsoft", 5 | "rewrite": "/provider/microsoft/index.html" 6 | }, 7 | { 8 | "route": "/getting-started/installation", 9 | "rewrite": "/getting-started/installation/index.html" 10 | }, 11 | { 12 | "route": "/provider/entra", 13 | "rewrite": "/provider/entra/index.html" 14 | }, 15 | { 16 | "route": "/provider/paypal", 17 | "rewrite": "/provider/paypal/index.html" 18 | }, 19 | { 20 | "route": "/provider/oidc", 21 | "rewrite": "/provider/oidc/index.html" 22 | }, 23 | { 24 | "route": "/provider/auth0", 25 | "rewrite": "/provider/auth0/index.html" 26 | }, 27 | { 28 | "route": "/provider/zitadel", 29 | "rewrite": "/provider/zitadel/index.html" 30 | }, 31 | { 32 | "route": "/provider/keycloak", 33 | "rewrite": "/provider/keycloak/index.html" 34 | }, 35 | { 36 | "route": "/provider/microsoft", 37 | "rewrite": "/provider/microsoft/index.html" 38 | }, 39 | { 40 | "route": "/provider/github", 41 | "rewrite": "/provider/github/index.html" 42 | }, 43 | { 44 | "route": "/hooks", 45 | "rewrite": "/hooks/index.html" 46 | }, 47 | { 48 | "route": "/server-utils/oidc-handlers", 49 | "rewrite": "/server-utils/oidc-handlers/index.html" 50 | }, 51 | { 52 | "route": "/server-utils/session-management", 53 | "rewrite": "/server-utils/session-management/index.html" 54 | }, 55 | { 56 | "route": "/server-utils/middleware", 57 | "rewrite": "/server-utils/middleware/index.html" 58 | }, 59 | { 60 | "route": "/contributing", 61 | "rewrite": "/contributing/index.html" 62 | }, 63 | { 64 | "route": "/configuration", 65 | "rewrite": "/configuration/index.html" 66 | }, 67 | { 68 | "route": "/getting-started/providers", 69 | "rewrite": "/getting-started/providers/index.html" 70 | }, 71 | { 72 | "route": "/getting-started/security", 73 | "rewrite": "/getting-started/security/index.html" 74 | }, 75 | { 76 | "route": "/composable", 77 | "rewrite": "/composable/index.html" 78 | }, 79 | { 80 | "route": "/provider", 81 | "rewrite": "/provider/index.html" 82 | }, 83 | { 84 | "route": "/getting-started", 85 | "rewrite": "/getting-started/index.html" 86 | }, 87 | { 88 | "route": "/dev-mode", 89 | "rewrite": "/dev-mode/index.html" 90 | }, 91 | { 92 | "route": "/", 93 | "rewrite": "/index.html" 94 | } 95 | ], 96 | "platform": { 97 | "apiRuntime": "node:20" 98 | }, 99 | "navigationFallback": { 100 | "rewrite": "/api/server" 101 | } 102 | } -------------------------------------------------------------------------------- /src/runtime/providers/entra.ts: -------------------------------------------------------------------------------- 1 | import { parseURL } from 'ufo' 2 | import { createProviderFetch, defineOidcProvider } from '../server/utils/provider' 3 | 4 | type EntraIdRequiredFields = 'clientId' | 'clientSecret' | 'authorizationUrl' | 'tokenUrl' | 'redirectUri' 5 | 6 | interface EntraProviderConfig { 7 | /** 8 | * The resource identifier for the requested resource. 9 | * @default undefined 10 | */ 11 | resource?: string 12 | /** 13 | * The audience for the token, typically the client ID. 14 | * @default undefined 15 | */ 16 | audience?: string 17 | /** 18 | * Indicates the type of user interaction that is required. Valid values are login, none, consent, and select_account. 19 | * @default undefined 20 | */ 21 | prompt?: 'login' | 'none' | 'consent' | 'select_account' 22 | /** 23 | * You can use this parameter to pre-fill the username and email address field of the sign-in page for the user. Apps can use this parameter during reauthentication, after already extracting the login_hint optional claim from an earlier sign-in. 24 | * @default undefined 25 | */ 26 | loginHint?: string 27 | /** 28 | * Enables sign-out to occur without prompting the user to select an account. To use logout_hint, enable the login_hint optional claim in your client application and use the value of the login_hint optional claim as the logout_hint parameter. 29 | * @default undefined 30 | */ 31 | logoutHint?: string 32 | /** 33 | * If included, the app skips the email-based discovery process that user goes through on the sign-in page, leading to a slightly more streamlined user experience. 34 | * @default undefined 35 | */ 36 | domainHint?: string 37 | } 38 | 39 | export const entra = defineOidcProvider({ 40 | tokenRequestType: 'form-urlencoded', 41 | logoutRedirectParameterName: 'post_logout_redirect_uri', 42 | grantType: 'authorization_code', 43 | scope: ['openid'], 44 | pkce: true, 45 | state: true, 46 | nonce: true, 47 | requiredProperties: [ 48 | 'clientId', 49 | 'clientSecret', 50 | 'authorizationUrl', 51 | 'tokenUrl', 52 | 'redirectUri', 53 | ], 54 | async openIdConfiguration(config: any) { 55 | const parsedUrl = parseURL(config.authorizationUrl) 56 | const tenantId = parsedUrl.pathname.split('/')[1] 57 | const customFetch = await createProviderFetch(config) 58 | const openIdConfig = await customFetch(`https://${parsedUrl.host}/${tenantId}/.well-known/openid-configuration${config.audience ? `?appid=${config.audience}` : ''}`) 59 | openIdConfig.issuer = [`https://${parsedUrl.host}/${tenantId}/v2.0`, openIdConfig.issuer] 60 | return openIdConfig 61 | }, 62 | sessionConfiguration: { 63 | expirationCheck: true, 64 | automaticRefresh: true, 65 | expirationThreshold: 1800, 66 | }, 67 | validateAccessToken: false, 68 | validateIdToken: true, 69 | }) 70 | -------------------------------------------------------------------------------- /docs/app/assets/nuxt-oidc-auth-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/singleSignOut.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { $fetch } from '@nuxt/test-utils' 3 | import { url } from '@nuxt/test-utils/e2e' 4 | import { expect, test } from '@nuxt/test-utils/playwright' 5 | 6 | test.use({ 7 | // @ts-expect-error Config overwrite 8 | nuxt: { 9 | rootDir: fileURLToPath(new URL('./fixtures/oidcApp', import.meta.url)), 10 | build: false, 11 | buildDir: fileURLToPath(new URL('./fixtures/oidcApp/', import.meta.url)), 12 | nuxtConfig: { 13 | runtimeConfig: { 14 | oidc: { 15 | providers: { 16 | keycloak: { 17 | logoutUrl: '', 18 | clientId: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID, 19 | clientSecret: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET, 20 | sessionConfiguration: { 21 | singleSignOut: true, 22 | }, 23 | }, 24 | }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }) 30 | 31 | test('single sign out same browser', async ({ page, goto }) => { 32 | const session = await $fetch('/api/_auth/session') 33 | expect(session).toStrictEqual({}) 34 | const provider = 'keycloak' 35 | const providerUrl = url(`/auth/login`) 36 | const context = page.context() 37 | const page2 = await context.newPage() 38 | await page2.goto(providerUrl) 39 | await page2.click(`button[name="${provider}"]`) 40 | await page2.waitForURL('http://localhost:8080/**') 41 | await page2.fill('input[name="username"]', 'testuser') 42 | await page2.fill('input[name="password"]', 'p@ssword') 43 | await page2.click('input[name="login"]') 44 | await page2.waitForURL(url('/')) 45 | await page.waitForTimeout(1000) 46 | await goto(url('/')) 47 | await page.click('button[name="logout"]') 48 | expect(page.url()).toMatch(/^http:\/\/localhost:8080/) 49 | expect(page2).toHaveURL(url('/auth/login')) 50 | }) 51 | 52 | test('single sign out different browser', async ({ page, browser, goto }) => { 53 | const provider = 'keycloak' 54 | const providerUrl = url(`/auth/login`) 55 | await goto(providerUrl) 56 | await page.click(`button[name="${provider}"]`) 57 | await page.fill('input[name="username"]', 'testuser') 58 | await page.fill('input[name="password"]', 'p@ssword') 59 | await page.click('input[name="login"]') 60 | await page.waitForURL(url('/')) 61 | const context2 = await browser.newContext() 62 | const page2 = await context2.newPage() 63 | await page2.goto(providerUrl) 64 | await page2.click(`button[name="${provider}"]`) 65 | await page2.fill('input[name="username"]', 'testuser') 66 | await page2.fill('input[name="password"]', 'p@ssword') 67 | await page2.click('input[name="login"]') 68 | await page2.waitForURL(url('/')) 69 | await page.click('button[name="logout"]') 70 | expect(page.url()).toMatch(/^http:\/\/localhost:8080/) 71 | expect(page2).toHaveURL(url('/auth/login')) 72 | await context2.close() 73 | }) 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![nuxt-oidc-auth-social-card](https://github.com/user-attachments/assets/77ab04f8-7823-4dee-bae4-841e46357d6e)](https://nuxt.com/modules/nuxt-oidc-auth) 2 | 3 | # Nuxt OIDC Auth 4 | 5 | [![npm version][npm-version-src]][npm-version-href] 6 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 7 | [![License][license-src]][license-href] 8 | [![Nuxt][nuxt-src]][nuxt-href] 9 | 10 | Welcome to __Nuxt OIDC Auth__, a Nuxt module focusing on native OIDC (OpenID Connect) based authentication for Nuxt with a high level of customizability and security for SSR applications. 11 | This module doesn't use any external dependencies outside of the [unjs](https://unjs.io/) ecosystem except for token validation (the well known and tested `jose` library for JWT interactions). 12 | 13 | 👉 [Documentation](https://nuxtoidc.cloud/) 14 | 15 | ## Features 16 | 17 | ↩️  Automatic session and token renewal
18 | 💾  Encrypted server side refresh/access token storage powered by Nitro storage
19 | 🔑  Token validation
20 | 🔒  Secured & sealed cookies sessions
21 | ⚙️  Presets for popular OIDC providers
22 | 📤  Global middleware with automatic redirection to default provider or a custom login page (see playground)
23 | 👤  `useOidcAuth` composable for getting the user information, logging in and out, refetching the current session and triggering a token refresh
24 | 🗂️  Multi provider support with auto registered routes (`/auth//login`, `/auth//logout`, `/auth//callback`)
25 | 📝  Generic spec OpenID compatible connect provider with fully configurable OIDC flow (state, nonce, PKCE, token request, ...)
26 | 🕙  Session expiration check
27 | 28 | ## Installation 29 | 30 | ### Add `nuxt-oidc-auth` dependency to your project 31 | 32 | With nuxi 33 | 34 | ```bash 35 | pnpm dlx nuxi@latest module add nuxt-oidc-auth 36 | ``` 37 | 38 | or manually 39 | 40 | ```bash 41 | pnpm add -D nuxt-oidc-auth 42 | ``` 43 | 44 | Add `nuxt-oidc-auth` to the `modules` section of `nuxt.config.ts` 45 | 46 | ```js 47 | export default defineNuxtConfig({ 48 | modules: [ 49 | 'nuxt-oidc-auth' 50 | ] 51 | }) 52 | ``` 53 | 54 | ## ⚠️ Disclaimer 55 | 56 | This module is still in development, feedback and contributions are welcome! Use at your own risk. 57 | 58 | 59 | [npm-version-src]: https://img.shields.io/npm/v/nuxt-oidc-auth?labelColor=18181B&color=28CF8D 60 | [npm-version-href]: https://npmjs.com/package/nuxt-oidc-auth 61 | 62 | [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-oidc-auth?labelColor=18181B&color=28CF8D 63 | [npm-downloads-href]: https://npmjs.com/package/nuxt-oidc-auth 64 | 65 | [license-src]: https://img.shields.io/npm/l/nuxt-oidc-auth?labelColor=18181B&color=28CF8D 66 | [license-href]: https://npmjs.com/package/nuxt-oidc-auth 67 | 68 | [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js 69 | [nuxt-href]: https://nuxt.com 70 | -------------------------------------------------------------------------------- /src/runtime/server/handler/logout.get.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event } from 'h3' 2 | import type { OAuthConfig, ProviderKeys, UserSession } from '../../types' 3 | import type { OidcProviderConfig } from '../utils/provider' 4 | import { useRuntimeConfig } from '#imports' 5 | import { eventHandler, getQuery, getRequestURL, sendRedirect } from 'h3' 6 | import { withQuery } from 'ufo' 7 | import * as providerPresets from '../../providers' 8 | import { configMerger, convertObjectToSnakeCase } from '../utils/oidc' 9 | import { clearUserSession, getUserSession } from '../utils/session' 10 | 11 | export function logoutEventHandler({ onSuccess }: OAuthConfig) { 12 | return eventHandler(async (event: H3Event) => { 13 | // TODO: Is this the best way to get the current provider? 14 | const provider = event.path.split('/')[2] as ProviderKeys 15 | const config = configMerger(useRuntimeConfig().oidc.providers[provider] as OidcProviderConfig, providerPresets[provider]) 16 | 17 | if (config.logoutUrl) { 18 | const logoutParams = getQuery(event) 19 | const logoutRedirectUri = logoutParams.logoutRedirectUri || config.logoutRedirectUri 20 | 21 | // Set logout_hint and id_token_hint dynamic parameters if specified. According to https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout 22 | const additionalLogoutParameters: Record = config.additionalLogoutParameters ? { ...config.additionalLogoutParameters } : {} 23 | if (config.additionalLogoutParameters) { 24 | let userSession: UserSession 25 | try { 26 | userSession = await getUserSession(event) 27 | } 28 | catch { 29 | return sendRedirect(event, `${getRequestURL(event).protocol}//${getRequestURL(event).host}`, 302) 30 | } 31 | Object.keys(config.additionalLogoutParameters).forEach((key) => { 32 | if (key === 'idTokenHint' && userSession.idToken) 33 | additionalLogoutParameters[key] = userSession.idToken 34 | if (key === 'logoutHint' && userSession.claims?.login_hint) 35 | additionalLogoutParameters[key] = userSession.claims.login_hint as string 36 | }) 37 | } 38 | const location = withQuery(config.logoutUrl, { 39 | ...(config.logoutRedirectParameterName && logoutRedirectUri) && { [config.logoutRedirectParameterName]: logoutRedirectUri }, 40 | ...config.additionalLogoutParameters && convertObjectToSnakeCase(additionalLogoutParameters), 41 | }) 42 | 43 | // Clear session 44 | await clearUserSession(event) 45 | return sendRedirect( 46 | event, 47 | location, 48 | 302, 49 | ) 50 | } 51 | // Clear session 52 | await clearUserSession(event) 53 | return onSuccess(event, { 54 | user: null, 55 | }) 56 | }) 57 | } 58 | 59 | export default logoutEventHandler({ 60 | async onSuccess(event) { 61 | return sendRedirect(event, `${getRequestURL(event).protocol}//${getRequestURL(event).host}`, 302) 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/1.index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Welcome to Nuxt OIDC Auth 4 | --- 5 | 6 | Welcome to __Nuxt OIDC Auth__, a Nuxt module focusing on native OIDC (OpenID Connect) based authentication for Nuxt with a high level of customizability and security for SSR applications. 7 | This module doesn't use any external dependencies outside of the [unjs](https://unjs.io/) ecosystem except for token validation (the well known and tested `jose` library for JWT interactions). 8 | This module's session implementation is based on [nuxt-auth-utils](https://github.com/Atinux/nuxt-auth-utils). 9 | 10 | ::callout 11 | This module and it's authors are in no way or form connected to the OpenID Foundation (OIDF). [More](#disclaimer) 12 | :: 13 | 14 | ## Nuxt OIDC Auth Features 15 | 16 | ### 🔒  Secure 17 | 18 | - Session expiration check based on token expiration 19 | - Automatic session renewal based on token expiration and refresh tokens 20 | - Secured & sealed cookies sessions 21 | - Access and ID token cryptographic validation (if supported by provider) 22 | - Encrypted server side refresh/access token storage powered Nitro storage layer 23 | 24 | ### ⚙️  Nuxt integrated 25 | 26 | - Global middleware with automatic redirection to default provider or custom login page 27 | - `useOidcAuth` composable for getting the user information, logging in and out, refetching the current session and triggering a token refresh 28 | - Server side session and middleware integration 29 | 30 | ### 📝  Compatible 31 | 32 | - Generic spec compatible OpenID connect provider with fully configurable OIDC flow (state, nonce, PKCE, token request, ...) 33 | - Presets for [popular OIDC providers](/provider) 34 | - Multi provider support with auto registered routes (`/auth//login`, `/auth//logout`, `/auth//callback`) 35 | 36 | ## Recent breaking changes 37 | 38 | Since 0.16.0, the data from the providers userInfo endpoint is written into `userInfo` on the user object instead of `providerInfo`. 39 | Please adjust your `nuxt.config.ts` and .env/environment files and configurations accordingly. 40 | If you are using the user object from the `useOidcAuth` composable change the access to `providerInfo` to `userInfo`. 41 | 42 | ## Disclaimer 43 | 44 | ### OpenID Connect 45 | 46 | This project and its authors are not affiliated with, endorsed by, or in any way officially connected with the OpenID Foundation (OIDF) or any of its subsidiaries or affiliates. Any references to OpenID or the OpenID Foundation are purely for descriptive purposes, and the use of their name does not imply any form of association or endorsement. 47 | 48 | Furthermore, the logo and branding used in this project are the property of the authors and are not derived from or related to any logos or trademarks of the OpenID Foundation. All rights to the project logo are owned exclusively by the authors. 49 | 50 | ### Status 51 | 52 | This module is still in development, feedback and contributions are welcome! Use at your own risk. 53 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 99 | -------------------------------------------------------------------------------- /test/providers.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { $fetch } from '@nuxt/test-utils' 3 | import { url } from '@nuxt/test-utils/e2e' 4 | import { expect, test } from '@nuxt/test-utils/playwright' 5 | import { providers } from './fixtures/providers' 6 | 7 | // TODO: Add test loop for each provider 8 | 9 | test.use({ 10 | // @ts-expect-error Config overwrite 11 | nuxt: { 12 | rootDir: fileURLToPath(new URL('./fixtures/oidcApp', import.meta.url)), 13 | build: false, 14 | buildDir: fileURLToPath(new URL('./fixtures/oidcApp/', import.meta.url)), 15 | nuxtConfig: { 16 | runtimeConfig: { 17 | oidc: { 18 | providers: { 19 | keycloak: { 20 | logoutUrl: '', 21 | clientId: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID, 22 | clientSecret: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET, 23 | }, 24 | }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }) 30 | 31 | test('redirects correctly', async () => { 32 | const rootUrl = url('/') 33 | const response = await fetch(rootUrl) 34 | expect(response.url).toMatch(url('/auth/login')) 35 | }) 36 | 37 | test('excluded page is rendered', async () => { 38 | const excludedUrl = url('/excluded') 39 | const response = await fetch(excludedUrl) 40 | expect(response.url).toMatch(url('/excluded')) 41 | const html = await $fetch(excludedUrl) 42 | expect(html).toContain('Excluded page') 43 | }) 44 | 45 | test('redirects to provider', async ({ page, goto }) => { 46 | const provider = 'keycloak' 47 | const providerUrl = url(`/auth/login`) 48 | await goto(providerUrl) 49 | await page.click(`button[name="${provider}"]`) 50 | await page.fill('input[name="username"]', 'testuser') 51 | await page.fill('input[name="password"]', 'p@ssword') 52 | await page.click('input[name="login"]') 53 | await page.waitForURL(url('/')) 54 | const currentProviderDiv = page.locator('div[name="currentProvider"]') 55 | expect(await currentProviderDiv.textContent()).toBe(provider) 56 | }) 57 | 58 | test('can refresh', async ({ page, goto }) => { 59 | const provider = 'keycloak' 60 | const providerUrl = url(`/auth/login`) 61 | await goto(providerUrl) 62 | await page.click(`button[name="${provider}"]`) 63 | await page.fill('input[name="username"]', 'testuser') 64 | await page.fill('input[name="password"]', 'p@ssword') 65 | await page.click('input[name="login"]') 66 | await page.waitForURL(url('/')) 67 | const updatedAt = Number(await page.locator('div[name="updatedAt"]').textContent()) 68 | await page.waitForTimeout(1000) 69 | await page.click('button[name="refresh"]') 70 | await page.waitForTimeout(100) 71 | const updatedAt2 = Number(await page.locator('div[name="updatedAt"]').textContent()) 72 | expect(updatedAt2).toBeGreaterThan(updatedAt) 73 | }) 74 | 75 | test('can logout', async ({ page, goto }) => { 76 | const provider = 'keycloak' 77 | const providerUrl = url(`/auth/login`) 78 | await goto(providerUrl) 79 | await page.click(`button[name="${provider}"]`) 80 | await page.fill('input[name="username"]', 'testuser') 81 | await page.fill('input[name="password"]', 'p@ssword') 82 | await page.click('input[name="login"]') 83 | await page.click('button[name="logout"]') 84 | expect(page.url()).toMatch(/^http:\/\/localhost:8080/) 85 | }) 86 | -------------------------------------------------------------------------------- /src/runtime/providers/microsoft.ts: -------------------------------------------------------------------------------- 1 | import { createProviderFetch, defineOidcProvider } from '../server/utils/provider' 2 | 3 | type MicrosoftRequiredFields = 'clientId' | 'clientSecret' 4 | 5 | interface MicrosoftAdditionalFields { 6 | /** 7 | * Optional. Indicates the type of user interaction that is required. Valid values are `login`, `none`, `consent`, and `select_account`. 8 | * @default 'login' 9 | */ 10 | prompt?: 'login' | 'none' | 'consent' | 'select_account' 11 | /** 12 | * Optional. You can use this parameter to pre-fill the username and email address field of the sign-in page for the user. Apps can use this parameter during reauthentication, after already extracting the login_hint optional claim from an earlier sign-in. 13 | * @default undefined 14 | */ 15 | loginHint?: string 16 | /** 17 | * Optional. Enables sign-out to occur without prompting the user to select an account. To use logout_hint, enable the login_hint optional claim in your client application and use the value of the login_hint optional claim as the logout_hint parameter. 18 | * @default undefined 19 | */ 20 | logoutHint?: string 21 | /** 22 | * Optional. If included, the app skips the email-based discovery process that user goes through on the sign-in page, leading to a slightly more streamlined user experience. 23 | * @default undefined 24 | */ 25 | domainHint?: string 26 | } 27 | 28 | interface MicrosoftProviderConfig { 29 | /** 30 | * Required. The tenant id is used to automatically configure the correct endpoint urls for the Microsoft provider to work. 31 | * @default 'login' 32 | */ 33 | tenantId: 'login' | 'none' | 'consent' | 'select_account' 34 | } 35 | 36 | export const microsoft = defineOidcProvider({ 37 | tokenRequestType: 'form-urlencoded', 38 | logoutRedirectParameterName: 'post_logout_redirect_uri', 39 | grantType: 'authorization_code', 40 | // scopeInTokenRequest: true, 41 | scope: ['openid', 'User.Read'], 42 | pkce: true, 43 | state: true, 44 | nonce: true, 45 | requiredProperties: [ 46 | 'clientId', 47 | 'clientSecret', 48 | 'authorizationUrl', 49 | 'tokenUrl', 50 | 'redirectUri', 51 | ], 52 | responseType: 'code id_token', 53 | async openIdConfiguration(config: any) { 54 | const customFetch = await createProviderFetch(config) 55 | const openIdConfig = await customFetch(`https://login.microsoftonline.com/${config.tenantId ? config.tenantId : 'common'}/v2.0/.well-known/openid-configuration`) 56 | openIdConfig.issuer = config.tenantId ? [`https://login.microsoftonline.com/${config.tenantId}/v2.0`, openIdConfig.issuer] : undefined 57 | return openIdConfig 58 | }, 59 | sessionConfiguration: { 60 | expirationCheck: true, 61 | automaticRefresh: true, 62 | expirationThreshold: 1800, 63 | }, 64 | skipAccessTokenParsing: true, 65 | validateAccessToken: false, 66 | validateIdToken: true, 67 | additionalAuthParameters: { 68 | prompt: 'select_account', 69 | }, 70 | optionalClaims: ['name', 'preferred_username'], 71 | baseUrl: 'https://login.microsoftonline.com/common', 72 | authorizationUrl: '/oauth2/v2.0/authorize', 73 | tokenUrl: '/oauth2/v2.0/token', 74 | userInfoUrl: 'https://graph.microsoft.com/v1.0/me', // https://graph.microsoft.com/oidc/userinfo" 75 | }) 76 | -------------------------------------------------------------------------------- /docs/content/2.provider/9.paypal.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PayPal 3 | description: PayPal provider documentation 4 | icon: i-simple-icons-paypal 5 | --- 6 | 7 | ## Feature/OIDC support 8 | 9 | ❌  PKCE
10 | ✅  Nonce
11 | ✅  State
12 | ❌  Access Token validation
13 | ❌  ID Token validation
14 | 15 | ## Introduction 16 | 17 | PayPal doesn't support modern security standards like PKCE and is lacking OIDC features like logout redirect functionality. 18 | The developer center can be confusing sometimes, make sure to use the correct users for testing and the right credentials. 19 | 20 | You have to enable the "Login with PayPal" functionality first before being able to use PayPal for login: 21 | 22 | ::contentImage{src="/content/paypal-enable_login.png" alt="Enable Login with PayPal"} 23 | :: 24 | 25 | ### Scopes 26 | 27 | By default the PayPal provider used the `openid` scope. If you need more use information, you can add standard OIDC scopes (like `profile` or `email`) or custom PayPal ones like `https://uri.paypal.com/services/paypalattributes`. 28 | Before adding additional scopes, make sure you have checked at least one of the following checkboxes. 29 | 30 | ::contentImage{src="/content/paypal-scopes.png" alt="PayPal Scopes"} 31 | :: 32 | 33 | ### Sandbox 34 | 35 | For testing, PayPal provides test accounts and endpoints that are separate from PayPals production infrastructure. 36 | 37 | ::callout{icon="i-carbon-warning-alt" color="amber"} 38 | For testing use the account with type **Personal** from **Testing Tools** -> **Sandbox Accounts**. Trying to use a business account will always fail to retrieve information from the userinfo endpoint. This is unfortunately undocumented. 39 | :: 40 | 41 | ## Provider specific parameters 42 | 43 | This providers doesn't have specific parameters. 44 | 45 | ## Example Configuration 46 | 47 | ::callout{icon="i-carbon-warning-alt" color="amber"} 48 | Never store sensitive values like your client secret in your Nuxt config. Our recommendation is to inject at least client id and client secret via. environment variables. 49 | :: 50 | 51 | ```typescript [nuxt.config.ts] 52 | paypal: { 53 | clientId: '', 54 | clientSecret: '', 55 | scope: ['openid', 'profile'], 56 | authorizationUrl: 'https://www.sandbox.paypal.com/signin/authorize?flowEntry=static', // Replace depending on sandbox or production environment 57 | tokenUrl: 'https://api-m.sandbox.paypal.com/v1/oauth2/token', // Replace depending on sandbox or production environment 58 | userInfoUrl: 'https://api-m.sandbox.paypal.com/v1/identity/openidconnect/userinfo?schema=openid', // Replace depending on sandbox or production environment 59 | redirectUri: 'http://127.0.0.1:3000/auth/paypal/callback', // PayPal doesn't support localhost for http, only 127.0.0.1 60 | }, 61 | ``` 62 | 63 | ### Environment variables 64 | 65 | Dotenv files are only for (local) development. Use a proper configuration management or injection system in production. 66 | 67 | PayPal only supports IPs for local development, so you have to set the redirectUri to 127.0.0.1 and set Nuxt to expose on that address via. the `.env` `HOST` entry. 68 | 69 | ::contentImage{src="/content/paypal-redirect.png" alt="PayPal Scopes"} 70 | :: 71 | 72 | ```ini [.env] 73 | # Only enable the HOST entry when testing PAYPAL 74 | HOST=127.0.0.1 75 | NUXT_OIDC_PROVIDERS_PAYPAL_CLIENT_ID=CLIENT_ID 76 | NUXT_OIDC_PROVIDERS_PAYPAL_CLIENT_SECRET=CLIENT_SECRET 77 | ``` -------------------------------------------------------------------------------- /src/runtime/server/handler/dev.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event } from 'h3' 2 | import type { OAuthConfig, UserSession } from '../../types' 3 | import { useRuntimeConfig } from '#imports' 4 | import { deleteCookie, eventHandler, sendRedirect } from 'h3' 5 | import { SignJWT } from 'jose' 6 | import { subtle } from 'uncrypto' 7 | import { useOidcLogger } from '../utils/oidc' 8 | import { generateRandomUrlSafeString } from '../utils/security' 9 | import { setUserSession, useAuthSession } from '../utils/session' 10 | 11 | export function devEventHandler({ onSuccess }: OAuthConfig) { 12 | const logger = useOidcLogger() 13 | return eventHandler(async (event: H3Event) => { 14 | logger.warn('Using dev auth handler with static auth information') 15 | 16 | const session = await useAuthSession(event) 17 | 18 | // Construct user object 19 | const timestamp = Math.trunc(Date.now() / 1000) // Use seconds instead of milliseconds to align with JWT 20 | const user: UserSession = { 21 | canRefresh: false, 22 | loggedInAt: timestamp, 23 | updatedAt: timestamp, 24 | expireAt: timestamp + 86400, // Adding one day 25 | provider: 'dev', 26 | userName: useRuntimeConfig().oidc.devMode?.userName || 'Nuxt OIDC Auth Dev', 27 | ...useRuntimeConfig().oidc.devMode?.userInfo && { userInfo: useRuntimeConfig().oidc.devMode?.userInfo }, 28 | ...useRuntimeConfig().oidc.devMode?.idToken && { idToken: useRuntimeConfig().oidc.devMode?.idToken }, 29 | ...useRuntimeConfig().oidc.devMode?.accessToken && { accessToken: useRuntimeConfig().oidc.devMode?.accessToken }, 30 | ...useRuntimeConfig().oidc.devMode?.claims && { claims: useRuntimeConfig().oidc.devMode?.claims }, 31 | } 32 | 33 | // Generate JWT dev token - Keys are only used in local dev mode, these are statically generated unsafe keys. 34 | if (useRuntimeConfig().oidc.devMode?.generateAccessToken) { 35 | let key 36 | let alg 37 | if (useRuntimeConfig().oidc.devMode?.tokenAlgorithm === 'asymmetric') { 38 | alg = 'RS256' 39 | const keyPair = await subtle.generateKey( 40 | { 41 | name: 'RSASSA-PKCS1-v1_5', 42 | modulusLength: 2048, 43 | publicExponent: new Uint8Array([0x01, 0x00, 0x01]), 44 | hash: { name: 'SHA-256' }, 45 | }, 46 | true, 47 | ['sign', 'verify'], 48 | ) 49 | key = keyPair.privateKey 50 | } 51 | else { 52 | alg = 'HS256' 53 | key = new TextEncoder().encode( 54 | generateRandomUrlSafeString(), 55 | ) 56 | } 57 | const jwt = await new SignJWT(useRuntimeConfig().oidc.devMode?.claims || {}) 58 | .setProtectedHeader({ alg }) 59 | .setIssuedAt() 60 | .setIssuer(useRuntimeConfig().oidc.devMode?.issuer || 'nuxt:oidc:auth:issuer') 61 | .setAudience(useRuntimeConfig().oidc.devMode?.audience || 'nuxt:oidc:auth:audience') 62 | .setExpirationTime('24h') 63 | .setSubject(useRuntimeConfig().oidc.devMode?.subject || 'nuxt:oidc:auth:subject') 64 | .sign(key) 65 | user.accessToken = jwt 66 | } 67 | 68 | await session.clear() 69 | deleteCookie(event, 'oidc') 70 | 71 | return onSuccess(event, { 72 | user, 73 | }) 74 | }) 75 | } 76 | 77 | export default devEventHandler({ 78 | async onSuccess(event, { user }) { 79 | await setUserSession(event, user as UserSession) 80 | return sendRedirect(event, '/') 81 | }, 82 | }) 83 | -------------------------------------------------------------------------------- /docs/content/2.provider/2.auth0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Auth0 3 | description: Auth0 provider documentation 4 | icon: i-simple-icons-auth0 5 | --- 6 | 7 | ## Feature/OIDC support 8 | 9 | ✅  PKCE
10 | ❌  Nonce
11 | ✅  State
12 | ✅  Access Token validation
13 | ❌  ID Token validation
14 | 15 | ## Introduction 16 | 17 | ## Provider specific parameters 18 | 19 | Additional parameters to be used in `additionalAuthParameters`, `additionalTokenParameters` or `additionalLogoutParameters`: 20 | 21 | | Option | Type | Default | Description | 22 | |---|---|---|---| 23 | | connection | `string` | - | Optional. Forces the user to sign in with a specific connection. For example, you can pass a value of github to send the user directly to GitHub to log in with their GitHub account. When not specified, the user sees the Auth0 Lock screen with all configured connections. You can see a list of your configured connections on the Connections tab of your application. | 24 | | organization | `string` | - | Optional. ID of the organization to use when authenticating a user. When not provided, if your application is configured to Display Organization Prompt, the user will be able to enter the organization name when authenticating. | 25 | | invitation | `string` | - | Optional. Ticket ID of the organization invitation. When inviting a member to an Organization, your application should handle invitation acceptance by forwarding the invitation and organization key-value pairs when the user accepts the invitation. | 26 | | loginHint | `string` | - | Optional. Populates the username/email field for the login or signup page when redirecting to Auth0. Supported by the Universal Login experience. | 27 | | audience | `string` | - | Optional. The unique identifier of the API your web app wants to access. | 28 | 29 | Depending on the settings of your apps `Credentials` tab, set `authenticationScheme` to `body` for 'Client Secret (Post)', set to `header` for 'Client Secret (Basic)', set to `''` for 'None' 30 | 31 | ## Example Configuration 32 | 33 | ::callout{icon="i-carbon-warning-alt" color="amber"} 34 | Never store sensitive values like your client secret in your Nuxt config. Our recommendation is to inject at least client id and client secret via. environment variables. 35 | :: 36 | 37 | ```typescript [nuxt.config.ts] 38 | auth0: { 39 | audience: 'test-api-oidc', // In case you need access to an API registered in Auth0 40 | redirectUri: 'http://localhost:3000/auth/auth0/callback', 41 | baseUrl: '', // For example https://dev-xyz.eu.auth0.com or leave blank for environment 42 | clientId: '', // Leave blank to use environment variable 43 | clientSecret: '', // Leave blank to use environment variable 44 | scope: ['openid', 'offline_access', 'profile', 'email'], 45 | additionalTokenParameters: { // In case you need access to an API registered in Auth0 46 | audience: 'test-api-oidc', 47 | }, 48 | additionalAuthParameters: { // In case you need access to an API registered in Auth0 49 | audience: 'test-api-oidc', 50 | }, 51 | }, 52 | ``` 53 | 54 | Make sure these grants are enabled in Auth0: 55 | 56 | ::contentImage{src="/content/auth0-grants.png" alt="Auth0 grant config"} 57 | :: 58 | 59 | ### Environment variables 60 | 61 | Dotenv files are only for (local) development. Use a proper configuration management or injection system in production. 62 | 63 | ```ini [.env] 64 | NUXT_OIDC_PROVIDERS_AUTH0_CLIENT_SECRET=CLIENT_SECRET 65 | NUXT_OIDC_PROVIDERS_AUTH0_CLIENT_ID=CLIENT_ID 66 | NUXT_OIDC_PROVIDERS_AUTH0_BASE_URL=https://dev-xyz.eu.auth0.com 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/app/assets/browser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/app/assets/landing.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 60 | -------------------------------------------------------------------------------- /docs/content/2.provider/6.keycloak.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: KeyCloak 3 | description: KeyCloak provider documentation 4 | icon: i-simple-icons-keycloak 5 | --- 6 | 7 | ## Feature/OIDC support 8 | 9 | ✅  PKCE
10 | ✅  Nonce
11 | ❌  State
12 | ✅  Access Token validation
13 | ❌  ID Token validation
14 | 15 | ## Introduction 16 | 17 | KeyCloak is an open-source identity and access management solution that provides features like single sign-on (SSO), social login, and user management, making it a popular choice for securing applications. This provider has tested defaults for KeyCloak to offer seamless OpenID Connect (OIDC) authentication with minimal necessary configuration. 18 | 19 | ## Example Configurations 20 | 21 | ::callout{icon="i-carbon-warning-alt" color="amber"} 22 | Never store sensitive values like your client secret in your Nuxt config. Our recommendation is to inject at least client id and client secret via. environment variables. 23 | :: 24 | 25 | ### Minimal 26 | 27 | ```typescript [nuxt.config.ts] 28 | keycloak: { 29 | audience: 'account', 30 | baseUrl: '', 31 | clientId: '', 32 | clientSecret: '', 33 | redirectUri: 'http://localhost:3000/auth/keycloak/callback', 34 | }, 35 | ``` 36 | 37 | ### Use logout url 38 | 39 | The to redirect to a specific url after logout, use the `logoutRedirectUri` configuration. 40 | You have to specifically allow a redirect uri, if you want your application to redirect to there after logout: 41 | 42 | ::contentImage{src="/content/keycloak-logoutredirect.png" alt="KeyCloak logout redirect"} 43 | :: 44 | 45 | ```typescript [nuxt.config.ts] 46 | keycloak: { 47 | audience: 'account', 48 | baseUrl: '', 49 | clientId: '', 50 | clientSecret: '', 51 | redirectUri: 'http://localhost:3000/auth/keycloak/callback', 52 | userNameClaim: 'preferred_username', 53 | logoutRedirectUri: 'http://localhost:3000', // Target of your post logout redirection 54 | }, 55 | ``` 56 | 57 | By default, the following settings are already set internally, but can be overwritten, if needed: 58 | 59 | - `logoutUrl`: `protocol/openid-connect/logout`, 60 | - `logoutRedirectParameterName`: `post_logout_redirect_uri`, 61 | 62 | ::callout{icon="i-carbon-warning-alt" color="amber"} 63 | If you want to use the post logout redirect feature, you should not set `exposeIdToken` to `false`, because the ID token is required to hand over to the post logout redirect url. 64 | :: 65 | 66 | ### Environment variables 67 | 68 | Dotenv files are only for (local) development. Use a proper configuration management or injection system in production. 69 | 70 | ```ini [.env] 71 | NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET=CLIENT_SECRET 72 | NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID=CLIENT_ID 73 | NUXT_OIDC_PROVIDERS_KEYCLOAK_BASE_URL=http://localhost:8080/realms/nuxt-oidc-test # For local keycloak instance 74 | ``` 75 | 76 | ## Provider specific parameters 77 | 78 | Additional parameters to be used in additionalAuthParameters, additionalTokenParameters or additionalLogoutParameters: 79 | 80 | | Option | Type | Default | Description | 81 | |---|---|---|---| 82 | | realm | `string` | - | Optional. This parameter allows to slightly customize the login flow on the KeyCloak server side. For example, enforce displaying the login screen in case of value login. | 83 | | realm | `string` | - | Optional. Used to pre-fill the username/email field on the login form. | 84 | | realm | `string` | - | Optional. Used to tell KeyCloak to skip showing the login page and automatically redirect to the specified identity provider instead. | 85 | | realm | `string` | - | Optional. Sets the 'ui_locales' query param. | 86 | 87 | For more information on these parameters, check the [KeyCloak documentation](https://www.keycloak.org/docs/latest/securing_apps/#methods). 88 | 89 | For KeyCloak you have to provide at least the `baseUrl`, `clientId` and `clientSecret` properties. The `baseUrl` is used to dynamically create the `authorizationUrl`, `tokenUrl`, `logoutUrl` and `userInfoUrl`. 90 | Please include the realm you want to use in the `baseUrl` (e.g. `https:///realms/`). 91 | If you don't want to use the post logout redirect feature of key cloak, set `logoutUrl` to `undefined` or `''`. 92 | Also remember to enable `Client authentication` to be able to get a client secret. 93 | -------------------------------------------------------------------------------- /src/runtime/server/handler/login.get.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event } from 'h3' 2 | import type { AuthorizationRequest, PkceAuthorizationRequest, ProviderKeys } from '../../types' 3 | import type { OidcProviderConfig } from '../utils/provider' 4 | import { useRuntimeConfig } from '#imports' 5 | import { eventHandler, getQuery, getRequestHeader, sendRedirect } from 'h3' 6 | import { withQuery } from 'ufo' 7 | import * as providerPresets from '../../providers' 8 | import { validateConfig } from '../utils/config' 9 | import { configMerger, convertObjectToSnakeCase, oidcErrorHandler, useOidcLogger } from '../utils/oidc' 10 | import { generatePkceCodeChallenge, generatePkceVerifier, generateRandomUrlSafeString } from '../utils/security' 11 | import { useAuthSession } from '../utils/session' 12 | 13 | function loginEventHandler() { 14 | const logger = useOidcLogger() 15 | return eventHandler(async (event: H3Event) => { 16 | const provider = event.path.split('/')[2] as ProviderKeys 17 | const config = configMerger(useRuntimeConfig().oidc.providers[provider] as OidcProviderConfig, providerPresets[provider]) 18 | const validationResult = validateConfig(config, config.requiredProperties) 19 | 20 | if (!validationResult.valid) { 21 | logger.error(`[${provider}] Missing configuration properties:`, validationResult.missingProperties?.join(', ')) 22 | oidcErrorHandler(event, 'Invalid configuration') 23 | } 24 | 25 | // Initialize auth session 26 | const session = await useAuthSession(event, config.sessionConfiguration?.maxAuthSessionAge) 27 | await session.clear() 28 | await session.update({ 29 | state: generateRandomUrlSafeString(), 30 | codeVerifier: generatePkceVerifier(), 31 | referer: getRequestHeader(event, 'referer'), 32 | nonce: undefined, 33 | }) 34 | 35 | // Get client side query parameters 36 | const additionalClientAuthParameters: Record = {} 37 | if (config.allowedClientAuthParameters?.length) { 38 | const clientQueryParams = getQuery(event) 39 | config.allowedClientAuthParameters.forEach((param) => { 40 | if (clientQueryParams[param]) { 41 | additionalClientAuthParameters[param] = clientQueryParams[param] as string 42 | } 43 | }) 44 | } 45 | 46 | let clientRedirectUri: string | undefined 47 | if (config.allowedCallbackRedirectUrls?.length) { 48 | const clientQueryParams = getQuery(event) 49 | if (clientQueryParams.redirectUri) { 50 | clientRedirectUri = config.allowedCallbackRedirectUrls.some(callbackUrl => (clientQueryParams.redirectUri as string).startsWith(callbackUrl)) ? clientQueryParams.redirectUri as string : undefined 51 | } 52 | if (clientRedirectUri) { 53 | await session.update({ redirect: clientRedirectUri }) 54 | } 55 | } 56 | 57 | const query: AuthorizationRequest | PkceAuthorizationRequest = { 58 | client_id: config.clientId, 59 | response_type: config.responseType, 60 | ...config.state && { state: session.data.state }, 61 | ...config.scope && { scope: config.scope.join(' ') }, 62 | ...config.responseMode && { response_mode: config.responseMode }, 63 | ...config.redirectUri && { redirect_uri: clientRedirectUri || config.redirectUri }, 64 | ...config.prompt && { prompt: config.prompt.join(' ') }, 65 | ...config.pkce && { code_challenge: await generatePkceCodeChallenge(session.data.codeVerifier), code_challenge_method: 'S256' }, 66 | ...config.additionalAuthParameters && convertObjectToSnakeCase(config.additionalAuthParameters), 67 | ...additionalClientAuthParameters && convertObjectToSnakeCase(additionalClientAuthParameters), 68 | } 69 | 70 | // Handling hybrid flows or mitigate replay attacks with nonce 71 | if (config.responseType.includes('token') || config.nonce) { 72 | const nonce = generateRandomUrlSafeString() 73 | await session.update({ nonce }) 74 | query.response_mode = 'form_post' 75 | query.nonce = nonce 76 | if (!query.scope?.includes('openid')) 77 | query.scope = `openid ${query.scope}` 78 | } 79 | 80 | return sendRedirect( 81 | event, 82 | config.encodeRedirectUri ? withQuery(config.authorizationUrl, query).replace(query.redirect_uri!, encodeURI(query.redirect_uri!)) : withQuery(config.authorizationUrl, query), 83 | 302, 84 | ) 85 | }) 86 | } 87 | 88 | export default loginEventHandler() 89 | -------------------------------------------------------------------------------- /src/runtime/composables/oidcAuth.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef } from '#imports' 2 | import type { ProviderKeys, UserSession } from '../types' 3 | import { computed, navigateTo, useRequestEvent, useRequestFetch, useState } from '#imports' 4 | import { appendResponseHeader } from 'h3' 5 | 6 | export function useOidcAuth() { 7 | const sessionState = useState('nuxt-oidc-auth-session', undefined) 8 | const user: ComputedRef = computed(() => sessionState.value ?? undefined) 9 | const loggedIn: ComputedRef = computed(() => { 10 | return Boolean(sessionState.value?.expireAt) 11 | }) 12 | const currentProvider: ComputedRef = computed(() => sessionState.value?.provider || undefined) 13 | const serverEvent = import.meta.server ? useRequestEvent() : null 14 | 15 | async function fetch() { 16 | sessionState.value = (await useRequestFetch()('/api/_auth/session', { 17 | headers: { 18 | Accept: 'text/json', 19 | }, 20 | }).catch(() => (undefined)) as UserSession) 21 | } 22 | 23 | /** 24 | * Manually refreshes the authentication session. 25 | * 26 | * @returns {Promise} 27 | */ 28 | async function refresh(): Promise { 29 | const currentProvider = sessionState.value?.provider || undefined 30 | sessionState.value = (await useRequestFetch()('/api/_auth/refresh', { 31 | headers: { 32 | Accept: 'text/json', 33 | }, 34 | method: 'POST', 35 | }).catch(() => login()) as UserSession) 36 | if (!loggedIn.value) { 37 | await logout(currentProvider) 38 | } 39 | } 40 | 41 | /** 42 | * Signs in the user by navigating to the appropriate sign-in URL. 43 | * 44 | * @param {ProviderKeys | 'dev'} [provider] - The authentication provider to use. If not specified, uses the default provider. 45 | * @param {Record} [params] - Additional parameters to include in the login request. Each parameters has to be listed in 'allowedClientAuthParameters' in the provider configuration. 46 | * @returns {Promise} 47 | */ 48 | async function login(provider?: ProviderKeys | 'dev', params?: Record): Promise { 49 | const queryParams = params ? `?${new URLSearchParams(params).toString()}` : '' 50 | await navigateTo(`/auth${provider ? `/${provider}` : ''}/login${queryParams}`, { external: true, redirectCode: 302 }) 51 | } 52 | 53 | /** 54 | * Logs out the user by navigating to the appropriate logout URL. 55 | * 56 | * @param {ProviderKeys | 'dev'} [provider] - The provider key or 'dev' for development. If provided, the user will be logged out from the specified provider. 57 | * @param {string} [logoutRedirectUri] - The URI to redirect to after logout if 'logoutRedirectParameterName' is set. If not provided, the user will be redirected to the root site. 58 | * @returns {Promise} 59 | */ 60 | async function logout(provider?: ProviderKeys | 'dev', logoutRedirectUri?: string): Promise { 61 | await navigateTo(`/auth${provider ? `/${provider}` : currentProvider.value ? `/${currentProvider.value}` : ''}/logout${logoutRedirectUri ? `?logout_redirect_uri=${logoutRedirectUri}` : ''}`, { external: true, redirectCode: 302 }) 62 | if (sessionState.value) { 63 | sessionState.value = undefined as unknown as UserSession 64 | } 65 | } 66 | 67 | /** 68 | * Clears the current user session. Mainly for debugging, in production, always use the `logout` function, which completely cleans the state. 69 | */ 70 | async function clear() { 71 | await useRequestFetch()(('/api/_auth/session'), { 72 | method: 'DELETE', 73 | headers: { 74 | Accept: 'text/json', 75 | }, 76 | onResponse({ response: { headers } }: { response: { headers: Headers } }) { 77 | // See https://github.com/atinux/nuxt-auth-utils/blob/main/src/runtime/app/composables/session.ts 78 | // Forward the Set-Cookie header to the main server event 79 | if (import.meta.server && serverEvent) { 80 | for (const setCookie of headers.getSetCookie()) { 81 | appendResponseHeader(serverEvent, 'Set-Cookie', setCookie) 82 | } 83 | } 84 | }, 85 | }) 86 | if (sessionState.value) { 87 | sessionState.value = undefined as unknown as UserSession 88 | } 89 | } 90 | 91 | return { 92 | loggedIn, 93 | user, 94 | currentProvider, 95 | fetch, 96 | refresh, 97 | login, 98 | logout, 99 | clear, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/fixtures/providers.ts: -------------------------------------------------------------------------------- 1 | export const providers = [ 2 | 'auth0', 3 | 'cognito', 4 | 'github', 5 | 'keycloak', 6 | 'logto', 7 | 'microsoft', 8 | 'oidc', 9 | 'paypal', 10 | 'zitadel', 11 | ] 12 | 13 | export const providerConfigs = { 14 | entra: { 15 | redirectUri: 'http://localhost:3000/auth/entra/callback', 16 | clientId: '', 17 | clientSecret: '', 18 | authorizationUrl: 'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize', 19 | tokenUrl: 'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token', 20 | userNameClaim: 'unique_name', 21 | nonce: true, 22 | responseType: 'code id_token', 23 | scope: ['profile', 'openid', 'offline_access', 'email'], 24 | logoutUrl: '', 25 | optionalClaims: ['unique_name', 'family_name', 'given_name', 'login_hint'], 26 | audience: '', 27 | additionalAuthParameters: { 28 | resource: '', 29 | prompt: 'select_account', 30 | }, 31 | additionalLogoutParameters: { 32 | logoutHint: '', 33 | }, 34 | allowedCallbackRedirectUrls: [ 35 | 'http://localhost:4000/auth/entra/callback', 36 | ], 37 | allowedClientAuthParameters: [ 38 | 'test', 39 | ], 40 | validateAccessToken: true, 41 | }, 42 | auth0: { 43 | audience: 'test-api-oidc', 44 | responseType: 'code', 45 | redirectUri: 'http://localhost:3000/auth/auth0/callback', 46 | baseUrl: '', 47 | clientId: '', 48 | clientSecret: '', 49 | scope: ['openid', 'offline_access', 'profile', 'email'], 50 | additionalTokenParameters: { 51 | audience: 'test-api-oidc', 52 | }, 53 | additionalAuthParameters: { 54 | audience: 'test-api-oidc', 55 | }, 56 | }, 57 | github: { 58 | redirectUri: 'http://localhost:3000/auth/github/callback', 59 | clientId: '', 60 | clientSecret: '', 61 | filterUserInfo: ['login', 'id', 'avatar_url', 'name', 'email'], 62 | }, 63 | keycloak: { 64 | audience: 'account', 65 | baseUrl: '', 66 | clientId: '', 67 | clientSecret: '', 68 | redirectUri: 'http://localhost:3000/auth/keycloak/callback', 69 | userNameClaim: 'preferred_username', 70 | logoutRedirectUri: 'http://localhost:3000', 71 | // For testing Single sign-out 72 | sessionConfiguration: { 73 | singleSignOut: true, 74 | }, 75 | allowedCallbackRedirectUrls: [ 76 | 'http://localhost', 77 | ], 78 | }, 79 | cognito: { 80 | clientId: '', 81 | redirectUri: 'http://localhost:3000/auth/cognito/callback', 82 | clientSecret: '', 83 | scope: ['openid', 'email', 'profile'], 84 | logoutRedirectUri: 'https://google.com', 85 | baseUrl: '', 86 | exposeIdToken: true, 87 | }, 88 | zitadel: { 89 | clientId: '', 90 | clientSecret: '', // Works with PKCE and Code flow, just leave empty for PKCE 91 | redirectUri: 'http://localhost:3000/auth/zitadel/callback', 92 | baseUrl: '', 93 | audience: '', // Specify for id token validation, normally same as clientId 94 | logoutRedirectUri: 'https://google.com', // Needs to be registered in Zitadel portal 95 | authenticationScheme: 'none', // Set this to 'header' if Code is used instead of PKCE 96 | }, 97 | paypal: { 98 | clientId: '', 99 | clientSecret: '', 100 | scope: ['openid', 'profile'], 101 | authorizationUrl: 'https://www.sandbox.paypal.com/signin/authorize?flowEntry=static', 102 | tokenUrl: 'https://api-m.sandbox.paypal.com/v1/oauth2/token', 103 | userInfoUrl: 'https://api-m.sandbox.paypal.com/v1/identity/openidconnect/userinfo?schema=openid', 104 | redirectUri: 'http://127.0.0.1:3000/auth/paypal/callback', 105 | }, 106 | microsoft: { 107 | clientId: '', 108 | clientSecret: '', 109 | redirectUri: 'http://localhost:3000/auth/microsoft/callback', 110 | }, 111 | logto: { 112 | baseUrl: '', 113 | clientId: '', 114 | clientSecret: '', 115 | redirectUri: 'http://localhost:3000/auth/logto/callback', 116 | logoutRedirectUri: 'http://localhost:3000', 117 | }, 118 | } 119 | 120 | export const middlewareConfig = { 121 | globalMiddlewareEnabled: true, 122 | customLoginPage: true, 123 | customLogoutPage: false, 124 | } 125 | 126 | export const sessionConfig = { 127 | expirationCheck: true, 128 | automaticRefresh: true, 129 | expirationThreshold: 3600, 130 | } 131 | 132 | export const devModeConfig = { 133 | enabled: false, 134 | generateAccessToken: true, 135 | userName: 'Test User', 136 | userInfo: { providerName: 'test' }, 137 | claims: { customclaim01: 'foo', customclaim02: 'bar' }, 138 | issuer: 'dev-issuer', 139 | audience: 'dev-app', 140 | subject: 'dev-user', 141 | } 142 | -------------------------------------------------------------------------------- /docs/app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 120 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/3.security.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Security 3 | description: Security is a main priority 4 | --- 5 | 6 | ## Background 7 | 8 | In this library, we’ve chosen to implement OAuth and OpenID flows from the ground up to provide flexibility and meet specific requirements and to be flexible. Not only has every OIDC providr it's own needs, but we also want to stay as close to the standards defined by [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) and [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749) specifications. 9 | 10 | However, we strictly avoided writing our own cryptographic functions, instead relying on the well-tested libraries [jose](https://github.com/panva/jose) for token validation and [noble-ciphers](https://github.com/paulmillr/noble-ciphers) for cryptographic operations. Both of which are 0-dependency libraries. 11 | 12 | This approach ensures that while we retain control over the authentication flows, we benefit from the security guarantees provided by audited and widely trusted cryptographic implementations, which are essential to avoid vulnerabilities and security flaws. 13 | 14 | ## OAuth 2.0 15 | 16 | This module only implements the `Authorization Code Flow` and optionally the `Hybrid Flow` in a confidential client scenario as detailed in the [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth). 17 | We will not support the `Implicit Flow` in the future, as it should not be used anymore and was practically superseded by the `Authorization Code Flow`. 18 | We will also not support the `Client Credential Flow`, as it is not part of OIDC, but of OAuth2 and is correctly named `Client Credentials Grant`. It is basically just an exchange of credentials for a token, is not meant for user authentication and can easily be implemented using a simple `fetch` request. 19 | 20 | ## Nuxt 21 | 22 | This module only works with SSR (server-side rendering) enabled as it uses server API routes. You cannot use this module with `nuxt generate` or `ssr` turned off. We are currently investigating if and how to support prerendering. 23 | 24 | ## Session encryption 25 | 26 | We store sensitive data, especially the refresh_token, access_token and id_token in an encrypted persistent session object. This library uses the [Nitro storage layer](https://nitro.unjs.io/guide/storage) to interact with data and uses the `oidc` namespace. 27 | Here is an example configuration. You can also replace the storage by any supported Nitro storage providers like redis, just keep in mind that the underlying storage has impact on the authentication process duration depending on its latency. 28 | 29 | ```typescript [nuxt.config.ts] 30 | nitro: { 31 | preset: 'node-server', 32 | storage: { // Use local file system storage for dev quick setup 33 | oidc: { 34 | driver: 'fs', 35 | base: 'oidcstorage', 36 | }, 37 | }, 38 | }, 39 | ``` 40 | 41 | ### Configure secrets 42 | 43 | Nuxt OIDC Auth uses three different secrets to encrypt the user session, the individual auth sessions and the persistent server side token store. You can set them using environment variables or in the `.env` file. 44 | All of the secrets are auto generated if not set, but should be set manually in production. This is especially important for the session storage, as it won't be accessible anymore if the secret changes, for example, after a server restart. 45 | 46 | If you need a reference how you could generate random secrets or keys, we created an example as a starting point: [Secrets generation example](https://stackblitz.com/edit/nuxt-oidc-auth-keygen?file=index.js) 47 | 48 | - NUXT_OIDC_TOKEN_KEY (random key): This needs to be a random cryptographic AES key in base64. Used to encrypt the server side token store. You can generate a key in JS with `await subtle.exportKey('raw', await subtle.generateKey({ name: 'AES-GCM', length: 256, }, true, ['encrypt', 'decrypt']))`. You just have to encode it to base64 afterwards. 49 | - NUXT_OIDC_SESSION_SECRET (random string): This should be a at least 48 characters random string. It is used to encrypt the user session. 50 | - NUXT_OIDC_AUTH_SESSION_SECRET (random string): This should be a at least 48 characters random string. It is used to encrypt the individual sessions during OAuth flows. 51 | 52 | Add a `NUXT_OIDC_SESSION_SECRET` env variable with at least 48 characters in the `.env` file. 53 | 54 | ```ini 55 | # .env 56 | NUXT_OIDC_TOKEN_KEY=base64_encoded_key 57 | NUXT_OIDC_SESSION_SECRET=48_characters_random_string 58 | NUXT_OIDC_AUTH_SESSION_SECRET=48_characters_random_string 59 | ``` 60 | 61 | ## Token validation 62 | 63 | ID and access token validation involves verifying the integrity and claims of the ID token (usually a JWT) to authenticate the user, and validating the access token by checking its signature, expiration, issuer, and audience to ensure it grants appropriate permissions for resource access. This is critical to prevent token tampering, misuse, and unauthorized access to protected APIs or services. 64 | 65 | We use the well known and tested jose library for token validation. 66 | Tokens will only be validated if the `clientId` or the optional audience (`aud`) field is part of the access_token (or id_token if existent) audiences. 67 | 68 | ::callout{icon="i-carbon-warning-alt" color="amber"} 69 | Even if `validateAccessToken` or `validateIdToken` is set, but the audience doesn't match, the token should not and will not be validated. 70 | :: 71 | 72 | Some providers like Entra or Zitadel don't or just in certain cases provide a parsable JWT access token. Validation will fail for these and should be disable, even if the audience is set. 73 | -------------------------------------------------------------------------------- /docs/content/2.provider/8.microsoft.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Microsoft 3 | description: Microsoft provider documentation 4 | icon: i-simple-icons-microsoft 5 | --- 6 | 7 | ## Feature/OIDC support 8 | 9 | ✅  PKCE
10 | ✅  Nonce
11 | ✅  State
12 | ❌  Access Token validation
13 | ✅  ID Token validation
14 | 15 | ## Introduction 16 | 17 | This is the simplified Microsoft provider for social login with a Microsoft Account (MSA). You need access to the Azure portal (portal.azure.com) to configure an app registration and get the required properties. 18 | Learn how to creat an app registration [here](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?tabs=client-secret). 19 | Be sure that you select one of the bottom two options if you want persoanl Microsoft accounts to be able to login to your application: 20 | 21 | ::contentImage{src="/content/microsoft-accounttype.png" alt="Microsoft account types"} 22 | :: 23 | 24 | Choose 'Web' as the target platform under `Manage` -> `Authentication` -> `Platform configurations` 25 | 26 | ::contentImage{src="/content/microsoft-platform.png" alt="Microsoft Platform configurations"} 27 | :: 28 | 29 | This provider uses the predefined userInfo url `https://graph.microsoft.com/v1.0/me` to get user information for an account. 30 | 31 | ## Example Configurations 32 | 33 | ::callout{icon="i-carbon-warning-alt" color="amber"} 34 | Never store sensitive values like your client secret in your Nuxt config. Our recommendation is to inject at least client id and client secret via. environment variables. 35 | :: 36 | 37 | ### Minimal 38 | 39 | ```typescript [nuxt.config.ts] 40 | entra: { 41 | redirectUri: 'http://localhost:3000/auth/microsoft/callback', 42 | clientId: '', 43 | clientSecret: '', 44 | }, 45 | ``` 46 | 47 | ### Get user information 48 | 49 | You can also add the `profile` or another OIDC common scope. `User.Read` as a delegated permission is configured by default in **API permissons**. 50 | 51 | ```typescript [nuxt.config.ts] 52 | entra: { 53 | redirectUri: 'http://localhost:3000/auth/microsoft/callback', 54 | clientId: '', 55 | clientSecret: '', 56 | responseType: 'code', 57 | scope: ['openid', 'User.Read'], 58 | }, 59 | ``` 60 | 61 | ### Get additonal user information with ID token 62 | 63 | To be able to use the ID token, make sure you have set the checkbox at **Manage** -> **Authentication** -> **Implicit grant and hybrid flows** -> `ID tokens (used for implicit and hybrid flows)`. 64 | To add additional claims you want to use, configure them on your app registration under **Manage** -> **Token configuration** and add them by using the `optionalClaims: ['name', 'preferred_username'],` parameter. 65 | The default setting is `optionalClaims: ['name', 'preferred_username'],`. 66 | 67 | ```typescript [nuxt.config.ts] 68 | entra: { 69 | redirectUri: 'http://localhost:3000/auth/microsoft/callback', 70 | clientId: '', 71 | clientSecret: '', 72 | responseType: 'code id_token', 73 | scope: ['openid', 'User.Read'], 74 | }, 75 | ``` 76 | 77 | ### Offline access/refresh token 78 | 79 | In order to get a refresh token, you need to add the "Microsoft Graph" delegated permission `offline_access` and add it to the scopes. 80 | **Manage** -> **API permissions** -> `Add a permission` 81 | 82 | ::contentImage{src="/content/microsoft-scopes.png" alt="Microsoft account types"} 83 | :: 84 | 85 | ```typescript [nuxt.config.ts] 86 | entra: { 87 | redirectUri: 'http://localhost:3000/auth/microsoft/callback', 88 | clientId: '', 89 | clientSecret: '', 90 | responseType: 'code id_token', 91 | scope: ['openid', 'User.Read', 'offline_access'], 92 | }, 93 | ``` 94 | 95 | ### Environment variables 96 | 97 | Dotenv files are only for (local) development. Use a proper configuration management or injection system in production. 98 | 99 | ```ini [.env] 100 | NUXT_OIDC_PROVIDERS_MICROSOFT_CLIENT_SECRET=CLIENT_SECRET 101 | NUXT_OIDC_PROVIDERS_MICROSOFT_CLIENT_ID=CLIENT_ID 102 | ``` 103 | 104 | ## Provider specific parameters 105 | 106 | | Option | Type | Default | Description | 107 | |---|---|---|---| 108 | | tenantId | `string` | - | Required. The tenant id is used to automatically configure the correct endpoint urls for the Microsoft provider to work. | 109 | | prompt | `string` | 'login' | Optional. Indicates the type of user interaction that is required. Valid values are `login`, `none`, `consent`, and `select_account`. Can be used in `additionalAuthParameters`, `additionalTokenParameters` or `additionalLogoutParameters`. | 110 | | loginHint | `string` | - | Optional. You can use this parameter to pre-fill the username and email address field of the sign-in page for the user. Apps can use this parameter during reauthentication, after already extracting the login_hint optional claim from an earlier sign-in. Can be used in `additionalAuthParameters`, `additionalTokenParameters` or `additionalLogoutParameters`. | 111 | | logoutHint | `string` | - | Optional. Enables sign-out to occur without prompting the user to select an account. To use logout_hint, enable the login_hint optional claim in your client application and use the value of the login_hint optional claim as the logout_hint parameter. Can be used in `additionalAuthParameters`, `additionalTokenParameters` or `additionalLogoutParameters`. | 112 | | domainHint | `string` | - | Optional. If included, the app skips the email-based discovery process that user goes through on the sign-in page, leading to a slightly more streamlined user experience. Can be used in `additionalAuthParameters`, `additionalTokenParameters` or `additionalLogoutParameters`. | 113 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | 3 | export default defineNuxtConfig({ 4 | modules: [ 5 | '../src/module', 6 | '@unocss/nuxt', 7 | '@nuxtjs/color-mode', 8 | ], 9 | 10 | telemetry: false, 11 | 12 | oidc: { 13 | defaultProvider: 'github', 14 | providers: { 15 | entra: { 16 | redirectUri: 'http://localhost:3000/auth/entra/callback', 17 | clientId: '', 18 | clientSecret: '', 19 | authorizationUrl: 'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize', 20 | tokenUrl: 'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token', 21 | userNameClaim: 'unique_name', 22 | nonce: true, 23 | responseType: 'code id_token', 24 | scope: ['profile', 'openid', 'offline_access', 'email'], 25 | logoutUrl: '', 26 | optionalClaims: ['unique_name', 'family_name', 'given_name', 'login_hint'], 27 | audience: '', 28 | additionalAuthParameters: { 29 | resource: '', 30 | prompt: 'select_account', 31 | }, 32 | additionalLogoutParameters: { 33 | logoutHint: '', 34 | }, 35 | allowedCallbackRedirectUrls: [ 36 | 'http://localhost:4000/auth/entra/callback', 37 | ], 38 | allowedClientAuthParameters: [ 39 | 'test', 40 | ], 41 | validateAccessToken: true, 42 | }, 43 | auth0: { 44 | audience: 'test-api-oidc', 45 | responseType: 'code', 46 | redirectUri: 'http://localhost:3000/auth/auth0/callback', 47 | baseUrl: '', 48 | clientId: '', 49 | clientSecret: '', 50 | scope: ['openid', 'offline_access', 'profile', 'email'], 51 | additionalTokenParameters: { 52 | audience: 'test-api-oidc', 53 | }, 54 | additionalAuthParameters: { 55 | audience: 'test-api-oidc', 56 | }, 57 | }, 58 | github: { 59 | redirectUri: 'http://localhost:3000/auth/github/callback', 60 | clientId: '', 61 | clientSecret: '', 62 | filterUserInfo: ['login', 'id', 'avatar_url', 'name', 'email'], 63 | }, 64 | keycloak: { 65 | audience: 'account', 66 | baseUrl: '', 67 | clientId: '', 68 | clientSecret: '', 69 | redirectUri: 'http://localhost:3000/auth/keycloak/callback', 70 | userNameClaim: 'preferred_username', 71 | logoutRedirectUri: 'http://localhost:3000', 72 | // For testing Single sign-out 73 | sessionConfiguration: { 74 | singleSignOut: true, 75 | }, 76 | }, 77 | cognito: { 78 | clientId: '', 79 | redirectUri: 'http://localhost:3000/auth/cognito/callback', 80 | clientSecret: '', 81 | scope: ['openid', 'email', 'profile'], 82 | logoutRedirectUri: 'https://google.com', 83 | baseUrl: '', 84 | exposeIdToken: true, 85 | }, 86 | zitadel: { 87 | clientId: '', 88 | clientSecret: '', // Works with PKCE and Code flow, just leave empty for PKCE 89 | redirectUri: 'http://localhost:3000/auth/zitadel/callback', 90 | baseUrl: '', 91 | audience: '', // Specify for id token validation, normally same as clientId 92 | logoutRedirectUri: 'https://google.com', // Needs to be registered in Zitadel portal 93 | authenticationScheme: 'none', // Set this to 'header' if Code is used instead of PKCE 94 | }, 95 | paypal: { 96 | clientId: '', 97 | clientSecret: '', 98 | scope: ['openid', 'profile'], 99 | authorizationUrl: 'https://www.sandbox.paypal.com/signin/authorize?flowEntry=static', 100 | tokenUrl: 'https://api-m.sandbox.paypal.com/v1/oauth2/token', 101 | userInfoUrl: 'https://api-m.sandbox.paypal.com/v1/identity/openidconnect/userinfo?schema=openid', 102 | redirectUri: 'http://127.0.0.1:3000/auth/paypal/callback', 103 | }, 104 | microsoft: { 105 | clientId: '', 106 | clientSecret: '', 107 | redirectUri: 'http://localhost:3000/auth/microsoft/callback', 108 | }, 109 | logto: { 110 | baseUrl: '', 111 | clientId: '', 112 | clientSecret: '', 113 | redirectUri: 'http://localhost:3000/auth/logto/callback', 114 | logoutRedirectUri: 'http://localhost:3000', 115 | }, 116 | }, 117 | session: { 118 | expirationCheck: true, 119 | automaticRefresh: true, 120 | expirationThreshold: 3600, 121 | }, 122 | middleware: { 123 | globalMiddlewareEnabled: true, 124 | customLoginPage: true, 125 | }, 126 | devMode: { 127 | enabled: false, 128 | generateAccessToken: true, 129 | userName: 'Test User', 130 | userInfo: { providerName: 'test' }, 131 | claims: { customclaim01: 'foo', customclaim02: 'bar' }, 132 | issuer: 'dev-issuer', 133 | audience: 'dev-app', 134 | subject: 'dev-user', 135 | }, 136 | }, 137 | 138 | colorMode: { 139 | classSuffix: '', 140 | preference: 'dark', 141 | }, 142 | 143 | unocss: { 144 | preflight: true, 145 | configFile: 'uno.config.ts', 146 | }, 147 | 148 | devtools: { 149 | enabled: true, 150 | }, 151 | 152 | imports: { 153 | autoImport: true, 154 | }, 155 | 156 | nitro: { 157 | preset: 'node-server', 158 | storage: { // Local file system storage for demo purposes 159 | oidc: { 160 | driver: 'fs', 161 | base: 'playground/oidcstorage', 162 | }, 163 | }, 164 | }, 165 | 166 | compatibilityDate: '2024-08-28', 167 | }) 168 | -------------------------------------------------------------------------------- /src/runtime/server/utils/oidc.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event } from 'h3' 2 | import type { RefreshTokenRequest, TokenRequest, TokenRespose, UserSession } from '../../types' 3 | import { createConsola } from 'consola' 4 | import { createDefu } from 'defu' 5 | import { sendRedirect } from 'h3' 6 | import { snakeCase } from 'scule' 7 | import { normalizeURL } from 'ufo' 8 | import { textToBase64 } from 'undio' 9 | import { createProviderFetch, type OidcProviderConfig } from './provider' 10 | import { parseJwtToken } from './security' 11 | import { clearUserSession } from './session' 12 | 13 | export function useOidcLogger() { 14 | return createConsola().withDefaults({ tag: 'nuxt-oidc-auth', message: '[nuxt-oidc-auth]:' }) 15 | } 16 | 17 | // Custom defu config merger to replace default values instead of merging them, except for requiredProperties 18 | export const configMerger = createDefu((obj, key, value) => { 19 | if (Array.isArray(obj[key]) && Array.isArray(value)) { 20 | obj[key] = key === 'requiredProperties' ? Array.from(new Set(obj[key].concat(value))) : value as any 21 | return true 22 | } 23 | }) 24 | 25 | export async function refreshAccessToken(refreshToken: string, config: OidcProviderConfig) { 26 | const logger = useOidcLogger() 27 | const customFetch = await createProviderFetch(config) 28 | // Construct request header object 29 | const headers: HeadersInit = {} 30 | 31 | // Validate if authentication information should be send in header or body 32 | if (config.authenticationScheme === 'header') { 33 | const encodedCredentials = textToBase64(`${config.clientId}:${config.clientSecret}`, { dataURL: false }) 34 | headers.authorization = `Basic ${encodedCredentials}` 35 | } 36 | 37 | // Construct form data for refresh token request 38 | const requestBody: RefreshTokenRequest = { 39 | client_id: config.clientId, 40 | refresh_token: refreshToken, 41 | grant_type: 'refresh_token', 42 | ...(config.scopeInTokenRequest && config.scope) && { scope: config.excludeOfflineScopeFromTokenRequest ? config.scope.filter(s => s !== 'offline_access').join(' ') : config.scope.join(' ') }, 43 | ...(config.authenticationScheme === 'body') && { client_secret: normalizeURL(config.clientSecret) }, 44 | } 45 | // Make refresh token request 46 | let tokenResponse: TokenRespose 47 | try { 48 | tokenResponse = await customFetch( 49 | config.tokenUrl, 50 | { 51 | method: 'POST', 52 | headers, 53 | body: convertTokenRequestToType(requestBody, config.tokenRequestType), 54 | }, 55 | ) 56 | } 57 | catch (error: any) { 58 | throw new Error(error?.data ? `${error.data.error}: ${error.data.error_description}` : error) 59 | } 60 | 61 | // Construct tokens object 62 | const tokens: Record<'refreshToken' | 'accessToken' | 'idToken', string> = { 63 | refreshToken: tokenResponse.refresh_token || refreshToken, 64 | accessToken: tokenResponse.access_token, 65 | idToken: tokenResponse.id_token || '', 66 | } 67 | 68 | const accessToken = parseJwtToken(tokenResponse.access_token, !!config.skipAccessTokenParsing) 69 | 70 | // Construct user object 71 | const user: Omit = { 72 | canRefresh: !!tokens.refreshToken, 73 | updatedAt: Math.trunc(Date.now() / 1000), // Use seconds instead of milliseconds to align wih JWT 74 | expireAt: accessToken.exp || Math.trunc(Date.now() / 1000) + 3600, // Fallback 60 min 75 | } 76 | 77 | // Update optional claims 78 | if (config.optionalClaims && tokenResponse.id_token) { 79 | const parsedIdToken = parseJwtToken(tokenResponse.id_token) 80 | user.claims = {} 81 | config.optionalClaims.forEach(claim => parsedIdToken[claim] && ((user.claims as Record)[claim] = (parsedIdToken[claim]))) 82 | } 83 | 84 | logger.info('Successfully refreshed token') 85 | 86 | return { 87 | user, 88 | tokens, 89 | expiresIn: tokenResponse.expires_in, 90 | parsedAccessToken: accessToken, 91 | } 92 | } 93 | 94 | export function generateFormDataRequest(requestValues: RefreshTokenRequest | TokenRequest) { 95 | const requestBody = new FormData() 96 | Object.keys(requestValues).forEach((key) => { 97 | requestBody.append(key, normalizeURL(requestValues[key as keyof typeof requestValues] as string)) 98 | }) 99 | return requestBody 100 | } 101 | 102 | export function generateFormUrlEncodedRequest(requestValues: RefreshTokenRequest | TokenRequest) { 103 | const requestBody = new URLSearchParams() 104 | Object.entries(requestValues).forEach((key) => { 105 | if (typeof key[1] === 'string') 106 | requestBody.append(key[0], normalizeURL(key[1])) 107 | }) 108 | return requestBody 109 | } 110 | 111 | export function convertTokenRequestToType( 112 | requestValues: RefreshTokenRequest | TokenRequest, 113 | requestType: OidcProviderConfig['tokenRequestType'] = 'form', 114 | ) { 115 | switch (requestType) { 116 | case 'json': 117 | return requestValues 118 | case 'form-urlencoded': 119 | return generateFormUrlEncodedRequest(requestValues) 120 | default: 121 | return generateFormDataRequest(requestValues) 122 | } 123 | } 124 | 125 | export function convertObjectToSnakeCase(object: Record) { 126 | return Object.entries(object).reduce((acc, [key, value]) => { 127 | acc[snakeCase(key)] = value 128 | return acc 129 | }, {} as Record) 130 | } 131 | 132 | export async function oidcErrorHandler(event: H3Event, errorText: string, errorCode: number = 500) { 133 | const logger = useOidcLogger() 134 | await clearUserSession(event, true) 135 | logger.error(errorText, '- Code:', errorCode) 136 | return sendRedirect( 137 | event, 138 | '/', 139 | 302, 140 | ) 141 | } 142 | --------------------------------------------------------------------------------