├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── playground ├── app.css ├── components │ └── NavBar.vue ├── middleware │ └── auth.ts ├── nuxt.config.ts ├── package.json └── pages │ ├── [...slug].vue │ ├── demo │ └── index.vue │ ├── hello.vue │ ├── index.vue │ ├── info.vue │ ├── public.vue │ └── secure.vue ├── src ├── module.ts └── runtime │ ├── composables │ └── useOidc.ts │ ├── plugin.ts │ ├── server │ └── routes │ │ └── oidc │ │ ├── callback.ts │ │ ├── cbt.ts │ │ ├── error.ts │ │ ├── login.ts │ │ ├── logout.ts │ │ ├── status.ts │ │ └── user.ts │ ├── storage.ts │ └── utils │ ├── encrypt.ts │ ├── issueclient.ts │ ├── template.ts │ └── utils.ts ├── tsconfig.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NUXT_OPENID_CONNECT_OP_ISSUER="" 2 | NUXT_OPENID_CONNECT_OP_CLIENT_ID="" 3 | NUXT_OPENID_CONNECT_OP_CLIENT_SECRET="" 4 | NUXT_OPENID_CONNECT_OP_CALLBACK_URL="" 5 | NUXT_OPENID_CONNECT_CONFIG_COOKIE_FLAGS_ACCESS_TOKEN_SECURE="false" 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "@nuxtjs/eslint-config-typescript" 5 | ], 6 | "rules": { 7 | "@typescript-eslint/no-unused-vars": "off", 8 | "no-unused-vars": "off", 9 | "no-console": "off", 10 | "vue/multi-word-component-names": "off", 11 | "space-before-function-paren": "off" 12 | } 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | src/runtime/utils/user.ts 4 | 5 | # Logs 6 | *.log* 7 | 8 | # Temp directories 9 | .temp 10 | .tmp 11 | .cache 12 | 13 | # Yarn 14 | **/.yarn/cache 15 | **/.yarn/*state* 16 | 17 | # Generated dirs 18 | dist 19 | 20 | # Nuxt 21 | .nuxt 22 | .output 23 | .vercel_build_output 24 | .build-* 25 | .env 26 | .netlify 27 | 28 | # Env 29 | .env 30 | 31 | # Testing 32 | reports 33 | coverage 34 | *.lcov 35 | .nyc_output 36 | 37 | # VSCode 38 | .vscode 39 | 40 | # Intellij idea 41 | *.iml 42 | .idea 43 | 44 | # OSX 45 | .DS_Store 46 | .AppleDouble 47 | .LSOverride 48 | .AppleDB 49 | .AppleDesktop 50 | Network Trash Folder 51 | Temporary Items 52 | .apdisk 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt OpenID-Connect 2 | [![npm version](https://img.shields.io/npm/v/nuxt-openid-connect.svg?style=flat)](https://www.npmjs.com/package/nuxt-openid-connect) 3 | 4 | OpenID-Connect(OIDC) integration module for nuxt 3.0. (V0.4.0+ support nuxt 3.0.0-stable.) 5 | 6 | ## Features 7 | 8 | - An [**Nuxt 3**](https://v3.nuxtjs.org) module (Note: nuxt 2.x not supported). 9 | - OIDC integration ( implemetation based on [openid-client](https://github.com/panva/node-openid-client) ). 10 | - [State Management](https://v3.nuxtjs.org/guide/features/state-management/), shared login user info. 11 | - OIDC provider config. 12 | - Encrypt userInfo cookie. 13 | - Support browser localStorage store userInfo, which keep user auth info after page refresh. Similar like [this](https://stackoverflow.com/questions/68174642/how-to-keep-user-authenticated-after-refreshing-the-page-in-nuxtjs). 14 | 15 | ## Why use this module 16 | 17 | - The official [auth](https://github.com/nuxt-community/auth-module/issues/1719) module doesn't support Nuxt 3.0 yet. 18 | - [nuxt-oidc](https://github.com/deko2369/nuxt-oidc) also not support Nuxt 3.0. 19 | 20 | ## How to use this module 21 | 22 | - Add to a project 23 | ```bash 24 | npx nuxi@latest module add nuxt-openid-connect 25 | ``` 26 | 27 | - Then, add `nuxt-openid-connect` module to nuxt.config.ts and change to your configs (`openidConnect`): 28 | ```ts 29 | export default defineNuxtConfig({ 30 | // runtime config for nuxt-openid-connect example -- you can use env variables see .env.example 31 | runtimeConfig: { 32 | openidConnect: { 33 | op: { 34 | issuer: '', 35 | clientId: '', 36 | clientSecret: '', 37 | callbackUrl: '', 38 | }, 39 | config: { 40 | cookieFlags: { 41 | access_token: { 42 | httpOnly: true, 43 | secure: false, 44 | } 45 | } 46 | } 47 | }, 48 | }, 49 | 50 | // add nuxt-openid-connect module here... 51 | modules: [ 52 | 'nuxt-openid-connect' 53 | ], 54 | 55 | // configuration for nuxt-openid-connect 56 | openidConnect: { 57 | addPlugin: true, 58 | op: { 59 | issuer: 'your_issuer_value', 60 | clientId: 'your_issuer_clientid', 61 | clientSecret: 'secret', 62 | callbackUrl: '', // deprecated from 0.8.0 63 | scope: [ 64 | 'email', 65 | 'profile', 66 | 'address' 67 | ] 68 | }, 69 | config: { 70 | debug: false, // optional, default is false 71 | response_type: 'id_token', // or 'code' 72 | secret: 'oidc._sessionid', 73 | isCookieUserInfo: false, // whether save userinfo into cookie. 74 | cookie: { loginName: '' }, 75 | cookiePrefix: 'oidc._', 76 | cookieEncrypt: true, 77 | cookieEncryptKey: 'bfnuxt9c2470cb477d907b1e0917oidc', // 32 78 | cookieEncryptIV: 'ab83667c72eec9e4', // 16 79 | cookieEncryptALGO: 'aes-256-cbc', 80 | cookieMaxAge: 24 * 60 * 60, // default one day 81 | cookieFlags: { // default is empty 82 | access_token: { 83 | httpOnly: true, 84 | secure: false, 85 | } 86 | } 87 | } 88 | } 89 | }) 90 | 91 | ``` 92 | 93 | - Useage in setup. 94 | 95 | ```ts 96 | const oidc = useOidc() 97 | ``` 98 | 99 | Here is an [usage example](https://github.com/aborn/nuxt-openid-connect/blob/main/playground/pages/index.vue). 100 | 101 | ## 💻 Development 102 | 103 | - Clone repository 104 | - Install dependencies using `yarn install` 105 | - Run `yarn dev:prepare` to generate type stubs. 106 | - Use `yarn run` to start [playground](./playground) in development mode. 107 | 108 | ## License 109 | 110 | [MIT](./LICENSE) - Made with 💚 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-openid-connect", 3 | "version": "0.8.1", 4 | "description": "An nuxt 3 module with OpenID-Connect(OIDC) integration.", 5 | "keywords": [ 6 | "nuxt", 7 | "oidc", 8 | "module", 9 | "nuxt 3.0", 10 | "auth" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/aborn/nuxt-openid-connect.git" 15 | }, 16 | "license": "MIT", 17 | "type": "module", 18 | "exports": { 19 | ".": { 20 | "import": "./dist/module.mjs", 21 | "require": "./dist/module.cjs" 22 | } 23 | }, 24 | "main": "./dist/module.cjs", 25 | "types": "./dist/types.d.ts", 26 | "files": [ 27 | "dist" 28 | ], 29 | "scripts": { 30 | "prepack": "nuxt-module-build build", 31 | "dev": "nuxi dev playground", 32 | "dev:build": "nuxi build playground", 33 | "dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground", 34 | "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags", 35 | "lint": "eslint .", 36 | "test": "vitest run", 37 | "pub": "npm publish --access public" 38 | }, 39 | "dependencies": { 40 | "@nuxt/kit": "^3.11.2", 41 | "defu": "^6.0.0", 42 | "openid-client": "^5.1.6", 43 | "uuid": "^10.0.0" 44 | }, 45 | "devDependencies": { 46 | "@nuxt/module-builder": "^0.7.1", 47 | "@nuxtjs/eslint-config-typescript": "^12.1.0", 48 | "@types/node": "^20.11.10", 49 | "@types/uuid": "^8.3.4", 50 | "eslint": "^9.4.0", 51 | "nuxt": "^3.11.2", 52 | "typescript": "^5.3.3" 53 | }, 54 | "bugs": { 55 | "url": "https://github.com/aborn/nuxt-openid-connect/issues" 56 | }, 57 | "homepage": "https://github.com/aborn/nuxt-openid-connect#readme", 58 | "author": "aborn" 59 | } 60 | -------------------------------------------------------------------------------- /playground/app.css: -------------------------------------------------------------------------------- 1 | button { 2 | padding: 0.5rem; 3 | } -------------------------------------------------------------------------------- /playground/components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 40 | -------------------------------------------------------------------------------- /playground/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware((to, from) => { 2 | if (import.meta.server) { return } 3 | const { $oidc } = useNuxtApp() 4 | if (!$oidc.isLoggedIn) { 5 | $oidc.login(to.fullPath) 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | 2 | export default defineNuxtConfig({ 3 | app: { 4 | baseURL: '/openid/', 5 | head: { 6 | title: 'OIDC', 7 | link: [ 8 | { 9 | rel: 'stylesheet', 10 | href: 'https://unpkg.com/@picocss/pico@latest/css/pico.min.css' 11 | } 12 | ] 13 | } 14 | }, 15 | 16 | modules: [ 17 | 'nuxt-openid-connect' 18 | ], 19 | 20 | runtimeConfig: { 21 | openidConnect: { 22 | op: { 23 | issuer: '', 24 | clientId: '', 25 | clientSecret: '', 26 | callbackUrl: '' 27 | }, 28 | config: { 29 | cookieFlags: { 30 | access_token: { 31 | httpOnly: true, 32 | secure: false 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | 39 | openidConnect: { 40 | addPlugin: true, 41 | op: { 42 | issuer: 'http://localhost:8080/realms/test', // change to your OP addrress 43 | clientId: 'testClient', 44 | clientSecret: 'cnuLA78epx8s8vMbRxcaiXbzlS4u8bSA', 45 | callbackUrl: 'http://localhost:3000/oidc/callback', // optional 46 | scope: [ 47 | 'email', 48 | 'profile', 49 | 'address' 50 | ] 51 | }, 52 | config: { 53 | debug: true, 54 | response_type: 'code', 55 | secret: 'oidc._sessionid', 56 | isCookieUserInfo: false, // whether save userinfo into cookie. 57 | cookie: { loginName: '' }, 58 | cookiePrefix: 'oidc._', 59 | cookieEncrypt: true, 60 | cookieEncryptKey: 'bfnuxt9c2470cb477d907b1e0917oidc', 61 | cookieEncryptIV: 'ab83667c72eec9e4', 62 | cookieEncryptALGO: 'aes-256-cbc', 63 | cookieMaxAge: 24 * 60 * 60, // default one day 64 | hasCookieRefreshExpireDate: false, // Set this to true if your provider has an refresh_expires_in date for the refresh token 65 | cookieRefreshDefaultMaxAge: 24 * 60 * 60, // default one day if the hasCookieRefreshExpireDate is false 66 | cookieFlags: { 67 | access_token: { 68 | httpOnly: true, 69 | secure: false 70 | } 71 | } 72 | } 73 | } 74 | }) 75 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "nuxt-openid-connect-playground" 4 | } 5 | -------------------------------------------------------------------------------- /playground/pages/[...slug].vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 12 | -------------------------------------------------------------------------------- /playground/pages/demo/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 15 | -------------------------------------------------------------------------------- /playground/pages/hello.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 26 | 27 | 40 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 39 | -------------------------------------------------------------------------------- /playground/pages/info.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /playground/pages/public.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 15 | -------------------------------------------------------------------------------- /playground/pages/secure.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url' 2 | import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit' 3 | import { defu } from 'defu' 4 | import { name, version } from '../package.json' 5 | 6 | export type CookieSerializeOptions = { 7 | domain?: string | undefined; 8 | encode?(value: string): string; 9 | expires?: Date | undefined; 10 | httpOnly?: boolean | undefined; 11 | maxAge?: number | undefined; 12 | path?: string | undefined; 13 | sameSite?: true | false | 'lax' | 'strict' | 'none' | undefined; 14 | secure?: boolean | undefined; 15 | } 16 | 17 | export type OidcProvider = { 18 | issuer: string, 19 | clientId: string, 20 | clientSecret: string, 21 | callbackUrl: string, 22 | scope: Array 23 | } 24 | 25 | export type Config = { 26 | secret: string, 27 | cookie: {}, 28 | cookiePrefix: string, 29 | cookieEncrypt: boolean, 30 | cookieEncryptKey: string, 31 | cookieEncryptIV: string, 32 | cookieEncryptALGO: string, 33 | cookieMaxAge: number, 34 | response_type: string, 35 | response_mode?: string, 36 | cookieFlags?: { 37 | [key: string]: CookieSerializeOptions, 38 | } 39 | debug?: boolean | undefined, 40 | } 41 | 42 | export interface ModuleOptions { 43 | addPlugin: boolean, 44 | op: OidcProvider, 45 | config: Config 46 | } 47 | 48 | export default defineNuxtModule({ 49 | meta: { 50 | name, 51 | version, 52 | configKey: 'openidConnect', 53 | compatibility: { 54 | // Semver version of supported nuxt versions 55 | nuxt: '>=3.0.0-rc.8' 56 | } 57 | }, 58 | defaults: { 59 | addPlugin: true, 60 | op: { 61 | issuer: '', 62 | clientId: '', 63 | clientSecret: '', 64 | callbackUrl: '', 65 | scope: [ 66 | ] 67 | }, 68 | // express-session configuration 69 | config: { 70 | debug: false, 71 | secret: 'oidc._sessionid', // process.env.OIDC_SESSION_SECRET 72 | cookie: {}, 73 | cookiePrefix: 'oidc._', 74 | cookieEncrypt: true, 75 | cookieEncryptKey: 'bfnuxt9c2470cb477d907b1e0917oidc', 76 | cookieEncryptIV: 'ab83667c72eec9e4', 77 | cookieEncryptALGO: 'aes-256-cbc', 78 | cookieMaxAge: 24 * 60 * 60, // default one day 79 | response_type: 'id_token', 80 | cookieFlags: {} 81 | } 82 | }, 83 | setup(options, nuxt) { 84 | console.log('[DEBUG MODE]: ', options.config.debug) 85 | console.debug('[WITHOUT ENV VARS] options:', options) 86 | 87 | const { resolve } = createResolver(import.meta.url) 88 | const resolveRuntimeModule = (path: string) => resolve('./runtime', path) 89 | 90 | nuxt.hook('nitro:config', (nitroConfig) => { 91 | // Add server handlers 92 | nitroConfig.handlers = nitroConfig.handlers || [] 93 | nitroConfig.handlers.push({ 94 | method: 'get', 95 | route: '/oidc/status', 96 | handler: resolveRuntimeModule('./server/routes/oidc/status') 97 | }) 98 | nitroConfig.handlers.push({ 99 | method: 'get', 100 | route: '/oidc/login', 101 | handler: resolveRuntimeModule('./server/routes/oidc/login') 102 | }) 103 | nitroConfig.handlers.push({ 104 | method: 'get', 105 | route: '/oidc/logout', 106 | handler: resolveRuntimeModule('./server/routes/oidc/logout') 107 | }) 108 | nitroConfig.handlers.push({ 109 | method: 'get', 110 | route: '/oidc/callback', 111 | handler: resolveRuntimeModule('./server/routes/oidc/callback') 112 | }) 113 | nitroConfig.handlers.push({ 114 | method: 'post', 115 | route: '/oidc/callback', 116 | handler: resolveRuntimeModule('./server/routes/oidc/callback') 117 | }) 118 | nitroConfig.handlers.push({ 119 | method: 'get', 120 | route: '/oidc/user', 121 | handler: resolveRuntimeModule('./server/routes/oidc/user') 122 | }) 123 | nitroConfig.handlers.push({ 124 | method: 'get', 125 | route: '/oidc/cbt', 126 | handler: resolveRuntimeModule('./server/routes/oidc/cbt') 127 | }) 128 | nitroConfig.handlers.push({ 129 | method: 'post', 130 | route: '/oidc/cbt', 131 | handler: resolveRuntimeModule('./server/routes/oidc/cbt') 132 | }) 133 | nitroConfig.handlers.push({ 134 | method: 'get', 135 | route: '/oidc/error', 136 | handler: resolveRuntimeModule('./server/routes/oidc/error') 137 | }) 138 | 139 | nitroConfig.externals = defu(typeof nitroConfig.externals === 'object' ? nitroConfig.externals : {}, { 140 | inline: [ 141 | // Inline module runtime in Nitro bundle 142 | resolve('./runtime') 143 | ] 144 | }) 145 | }) 146 | const publicOps = Object.assign({}, options.op) 147 | const cnfg = Object.assign({}, options.config) 148 | publicOps.clientSecret = '' 149 | cnfg.cookieEncryptALGO = '' 150 | cnfg.cookieEncryptIV = '' 151 | cnfg.cookieEncryptKey = '' 152 | 153 | nuxt.options.runtimeConfig.public.openidConnect = defu(nuxt.options.runtimeConfig.public.openidConnect, { 154 | op: publicOps, 155 | config: cnfg 156 | }) 157 | 158 | // openidConnect config will use in server 159 | nuxt.options.runtimeConfig.openidConnect = { 160 | ...options as any 161 | } 162 | 163 | if (options.addPlugin) { 164 | const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url)) 165 | nuxt.options.build.transpile.push(runtimeDir) 166 | addPlugin(resolve(runtimeDir, 'plugin')) 167 | 168 | nuxt.hook('imports:dirs', (dirs) => { 169 | dirs.push(resolve(runtimeDir, 'composables')) 170 | }) 171 | } 172 | } 173 | }) 174 | -------------------------------------------------------------------------------- /src/runtime/composables/useOidc.ts: -------------------------------------------------------------------------------- 1 | import { useNuxtApp } from '#app' 2 | import { type Oidc } from '../plugin' 3 | 4 | export default function useOidc(): Oidc { 5 | return useNuxtApp().$oidc 6 | } 7 | -------------------------------------------------------------------------------- /src/runtime/plugin.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin } from '#app' 2 | import { Storage } from './storage' 3 | import { isUnset, isSet, getCleanUrl } from './utils/utils' 4 | import { decrypt } from './utils/encrypt' 5 | import { useState, useFetch, useRuntimeConfig, useCookie } from '#imports' 6 | 7 | interface UseState { 8 | user: any, 9 | isLoggedIn: boolean 10 | } 11 | 12 | export class Oidc { 13 | private state: UseState // only this plugin. 14 | private $useState: any // State: Nuxt.useState (share state in all nuxt pages and components) https://v3.nuxtjs.org/guide/features/state-management 15 | public $storage: Storage // LocalStorage: Browser.localStorage (share state in all sites, use in page refresh.) 16 | 17 | constructor() { 18 | this.state = { user: {}, isLoggedIn: false } 19 | 20 | this.$useState = useState('useState', () => { return { user: {}, isLoggedIn: false } }) 21 | const { config } = useRuntimeConfig()?.public?.openidConnect 22 | 23 | const storageOption = { 24 | localStorage: true, 25 | prefix: config.cookiePrefix, 26 | ignoreExceptions: true 27 | } 28 | const storage = new Storage(storageOption) 29 | this.$storage = storage 30 | } 31 | 32 | get user() { 33 | const userInfoState = this.$useState.value.user 34 | const userInfoLS = this.$storage.getUserInfo() 35 | if ((isUnset(userInfoState))) { 36 | // console.log('load user from Browser.localStorge', userInfoState) 37 | return userInfoLS 38 | } else { 39 | // console.log('load user from Nuxt.useState', userInfoState) 40 | return userInfoState 41 | } 42 | // return this.state.user // not auto update 43 | } 44 | 45 | get isLoggedIn() { 46 | const isLoggedIn = this.$useState.value.isLoggedIn 47 | const isLoggedInLS = this.$storage.isLoggedIn() 48 | // console.log('isLoggedIn', isLoggedIn, isLoggedInLS) 49 | return isLoggedIn || isLoggedInLS 50 | } 51 | 52 | setUser(user: any) { 53 | this.state.user = user 54 | this.state.isLoggedIn = Object.keys(user).length > 0 55 | 56 | this.$useState.value.user = user 57 | this.$useState.value.isLoggedIn = Object.keys(user).length > 0 58 | 59 | this.$storage.setUserInfo(user) 60 | } 61 | 62 | async fetchUser() { 63 | try { 64 | if (import.meta.server) { 65 | // console.log('serve-render: fetchUser from cookie.') 66 | const { config } = useRuntimeConfig()?.openidConnect 67 | const userinfoCookie = useCookie(config.cookiePrefix + 'user_info') 68 | if (isSet(userinfoCookie) && userinfoCookie.value) { 69 | const userInfoStr = await decrypt(userinfoCookie.value, config) 70 | if (userInfoStr) { 71 | const userinfo = JSON.parse(userInfoStr) 72 | this.setUser(userinfo) 73 | } else { 74 | console.error('userInfoStr undefined!') 75 | } 76 | } else { 77 | // console.log('empty cookie') 78 | this.setUser({}) 79 | } 80 | } else { 81 | // this.$useState.value.user is set by server, and pass to client ? how achived it ? 82 | // console.log('client-render: fetchUser from server.') 83 | const { data, pending, refresh, error } = await useFetch('/oidc/user') 84 | this.setUser(data.value) 85 | // console.log('fetchUser from server-api call.', data.value) 86 | if (error && error.value) { 87 | console.error('failed to fetch user data: ', error.value) 88 | this.setUser({}) 89 | } 90 | } 91 | } catch (err) { 92 | console.error('@plugin.fetchUser error', err) 93 | } 94 | } 95 | 96 | login(redirect = '/') { 97 | if (import.meta.client) { 98 | const { app } = useRuntimeConfig() 99 | const params = new URLSearchParams({ redirect }) 100 | const link = '/oidc/login?' + params.toString() 101 | window.location.replace(getCleanUrl(app.baseURL + link)) 102 | // navigateTo({ path: link }) 103 | } 104 | } 105 | 106 | logout(redirect = '/') { 107 | // TODO clear user info when accessToken expired. 108 | if (import.meta.client) { 109 | const { app } = useRuntimeConfig() 110 | const params = new URLSearchParams({ redirect }) 111 | const link = '/oidc/logout?' + params.toString() 112 | 113 | this.$useState.value.user = {} 114 | this.$useState.value.isLoggedIn = false 115 | 116 | this.$storage.removeUserInfo() 117 | window.location.replace(getCleanUrl(app.baseURL + link)) 118 | // navigateTo({ path: link }) 119 | } 120 | } 121 | } 122 | 123 | export default defineNuxtPlugin((nuxtApp) => { 124 | // TODO: enable consola debug mode here instead of console.log 125 | // console.log('--- Nuxt plugin: nuxt-openid-connect!') 126 | const oidc = new Oidc() 127 | nuxtApp.provide('oidc', oidc) 128 | // console.log('--- Nuxt plugin: DEBUG MODE:' + useNuxtApp().ssrContext?.runtimeConfig.openidConnect.config.debug); 129 | oidc.fetchUser() // render both from server and client. 130 | }) 131 | -------------------------------------------------------------------------------- /src/runtime/server/routes/oidc/callback.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http' 2 | import { defineEventHandler, getCookie, setCookie, deleteCookie } from 'h3' 3 | import { initClient } from '../../../utils/issueclient' 4 | import { encrypt } from '../../../utils/encrypt' 5 | import { getRedirectUrl, getCallbackUrl, getDefaultBackUrl, getResponseMode, setCookieInfo, setCookieTokenAndRefreshToken, getCleanUrl } from '../../../utils/utils' 6 | import { useRuntimeConfig } from '#imports' 7 | 8 | export default defineEventHandler(async (event) => { 9 | console.log('---------oidc nitro --------------') 10 | const req = event.node.req 11 | const res = event.node.res 12 | console.log('[CALLBACK]: oidc/callback calling, method:' + req.method) 13 | const { app } = useRuntimeConfig() 14 | const baseUrl = app.baseURL 15 | 16 | let request = req 17 | if (req.method === 'POST') { 18 | // response_mode=form_post ('POST' method) 19 | const body = await readBody(event) 20 | request = { 21 | method: req.method, 22 | url: req.url, 23 | body 24 | } as unknown as http.IncomingMessage 25 | } 26 | 27 | const { op, config } = useRuntimeConfig().openidConnect 28 | const responseMode = getResponseMode(config) 29 | const sessionid = getCookie(event, config.secret) 30 | deleteCookie(event, config.secret) 31 | // Note: here not need add baseUrl, case in login already added baseUrl. 32 | const redirectUrl = getRedirectUrl(req.url) 33 | // console.log('---Callback. redirectUrl:' + redirectUrl) 34 | // console.log(' -- req.url:' + req.url + ' #method:' + req.method + ' #response_mode:' + responseMode) 35 | 36 | const callbackUrl = getCallbackUrl(redirectUrl, req.headers.host) 37 | const defCallBackUrl = getDefaultBackUrl(redirectUrl, req.headers.host) 38 | 39 | const issueClient = await initClient(op, req, [defCallBackUrl, callbackUrl]) 40 | const params = issueClient.callbackParams(request) 41 | 42 | if (params.access_token) { 43 | // Implicit ID Token Flow: access_token 44 | console.log('[CALLBACK]: has access_token in params, accessToken:' + params.access_token) 45 | await processUserInfo(params.access_token, null, event) 46 | res.writeHead(302, { Location: redirectUrl || baseUrl }) 47 | res.end() 48 | } else if (params.code) { 49 | // Authorization Code Flow: code -> access_token 50 | console.log('[CALLBACK]: has code in params, code:' + params.code + ' ,sessionid=' + sessionid) 51 | const tokenSet = await issueClient.callback(callbackUrl, params, { nonce: sessionid }) 52 | if (tokenSet.access_token) { 53 | await processUserInfo(tokenSet.access_token, tokenSet, event) 54 | } 55 | res.writeHead(302, { Location: redirectUrl || baseUrl }) 56 | res.end() 57 | } else { 58 | // Error dealing. 59 | // eslint-disable-next-line no-lonely-if 60 | if (params.error) { 61 | // redirct to auth failed error page. 62 | console.error('[CALLBACK]: error callback') 63 | console.error(params.error + ', error_description:' + params.error_description) 64 | res.writeHead(302, { Location: getCleanUrl(baseUrl + '/oidc/error') }) 65 | res.end() 66 | } else if (responseMode === 'fragment') { 67 | console.warn('[CALLBACK]: callback redirect') 68 | res.writeHead(302, { Location: getCleanUrl(baseUrl + '/oidc/cbt?redirect=' + redirectUrl) }) 69 | res.end() 70 | } else { 71 | console.error('[CALLBACK]: error callback') 72 | res.writeHead(302, { Location: redirectUrl || baseUrl }) 73 | res.end() 74 | } 75 | } 76 | 77 | async function processUserInfo(accessToken: string, tokenSet: any, event: any) { 78 | try { 79 | const userinfo = await issueClient.userinfo(accessToken) 80 | const { config } = useRuntimeConfig().openidConnect 81 | 82 | // token and refresh token setting 83 | if (tokenSet) { 84 | setCookieTokenAndRefreshToken(event, config, tokenSet) 85 | } 86 | 87 | // userinfo setting 88 | await setCookieInfo(event, config, userinfo) 89 | } catch (err) { 90 | console.error('[CALLBACK]: ' + err) 91 | } 92 | } 93 | }) 94 | -------------------------------------------------------------------------------- /src/runtime/server/routes/oidc/cbt.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, setCookie, getCookie } from 'h3' 2 | import { CBT_PAGE_TEMPATE } from '../../../utils/template' 3 | import { useRuntimeConfig } from '#imports' 4 | 5 | export default defineEventHandler((event) => { 6 | console.log('[CBT]: oidc/cbt calling') 7 | const { config } = useRuntimeConfig().openidConnect 8 | const res = event.node.res 9 | const html = CBT_PAGE_TEMPATE 10 | 11 | const sessionkey = config.secret 12 | const sessionid = getCookie(event, sessionkey) 13 | // logger.debug('[CBT]:' + sessionid) 14 | /* setCookie(event, sessionkey, sessionid, { 15 | maxAge: 24 * 60 * 60 // oneday 16 | }) */ 17 | 18 | res.writeHead(200, { 19 | 'Content-Type': 'text/html', 20 | 'Content-Length': html.length, 21 | Expires: new Date().toUTCString() 22 | }) 23 | res.end(html) 24 | }) 25 | -------------------------------------------------------------------------------- /src/runtime/server/routes/oidc/error.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3' 2 | 3 | export default defineEventHandler((event) => { 4 | const req = event.node.req 5 | return { 6 | error: 'oidc auth failed!' 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /src/runtime/server/routes/oidc/login.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, setCookie, getCookie } from 'h3' 2 | import { v4 as uuidv4 } from 'uuid' 3 | import { generators } from 'openid-client' 4 | import { initClient } from '../../../utils/issueclient' 5 | import { getRedirectUrl, getCallbackUrl, getDefaultBackUrl, getResponseMode } from '../../../utils/utils' 6 | import { useRuntimeConfig } from '#imports' 7 | 8 | export default defineEventHandler(async (event) => { 9 | console.log('---------oidc nitro --------------') 10 | console.log('[Login]: oidc/login calling') 11 | const req = event.node.req 12 | const res = event.node.res 13 | const { app } = useRuntimeConfig() 14 | const baseUrl = app.baseURL 15 | 16 | const { op, config } = useRuntimeConfig().openidConnect 17 | const redirectUrl = getRedirectUrl(req.url, baseUrl) 18 | const callbackUrl = getCallbackUrl(redirectUrl, req.headers.host) 19 | const defCallBackUrl = getDefaultBackUrl(redirectUrl, req.headers.host) 20 | 21 | const issueClient = await initClient(op, req, [defCallBackUrl, callbackUrl]) 22 | const sessionkey = config.secret 23 | let sessionid = getCookie(event, config.secret) 24 | if (!sessionid) { 25 | sessionid = generators.nonce() 26 | console.log('[Login]: regenerate sessionid=' + sessionid) 27 | } else { 28 | console.log('[Login]: cookie sessionid=' + sessionid) 29 | } 30 | 31 | const responseMode = getResponseMode(config) 32 | const scopes = op.scope.includes('openid') ? op.scope : [...op.scope, 'openid'] 33 | console.log('[Login]: cabackurl & op.callbackUrl & redirecturl: ', callbackUrl, op.callbackUrl, redirectUrl) 34 | console.log(' response_mode:' + responseMode + ', response_type:' + config.response_type + ', scopes:' + scopes.join(' ')) 35 | 36 | const parameters = { 37 | redirect_uri: callbackUrl, 38 | response_type: config.response_type, 39 | response_mode: responseMode, 40 | nonce: sessionid, 41 | scope: scopes.join(' ') 42 | } 43 | const authUrl = issueClient.authorizationUrl(parameters) 44 | console.log('[Login]: Auth Url: ' + authUrl + ', #sessionid:' + sessionid) 45 | 46 | if (sessionid) { 47 | setCookie(event, sessionkey, sessionid, { 48 | maxAge: config.cookieMaxAge, 49 | ...config.cookieFlags[sessionkey as keyof typeof config.cookieFlags] 50 | }) 51 | } 52 | 53 | res.writeHead(302, { Location: authUrl }) 54 | res.end() 55 | }) 56 | -------------------------------------------------------------------------------- /src/runtime/server/routes/oidc/logout.ts: -------------------------------------------------------------------------------- 1 | import { getCookie, deleteCookie, defineEventHandler } from 'h3' 2 | import { getRedirectUrl } from '../../../utils/utils'; 3 | import { useRuntimeConfig } from '#imports' 4 | 5 | export default defineEventHandler((event) => { 6 | console.log('---------oidc nitro --------------') 7 | const res = event.node.res 8 | const req = event.node.req 9 | const { app } = useRuntimeConfig() 10 | const baseUrl = app.baseURL 11 | 12 | console.log('[LOGOUT]: oidc/logout calling') 13 | 14 | const { config } = useRuntimeConfig().openidConnect 15 | const redirectUrl = getRedirectUrl(req.url, baseUrl) 16 | deleteCookie(event, config.secret) 17 | deleteCookie(event, config.cookiePrefix + 'access_token') 18 | deleteCookie(event, config.cookiePrefix + 'refresh_token') 19 | deleteCookie(event, config.cookiePrefix + 'user_info') 20 | 21 | // delete part of cookie userinfo (depends on user's setting.). 22 | const cookie = config.cookie 23 | if (cookie) { 24 | for (const [key, value] of Object.entries(cookie)) { 25 | deleteCookie(event, config.cookiePrefix + key) 26 | } 27 | } 28 | 29 | res.writeHead(302, { Location: redirectUrl }) 30 | res.end() 31 | }) 32 | -------------------------------------------------------------------------------- /src/runtime/server/routes/oidc/status.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3' 2 | import { useRuntimeConfig } from '#imports' 3 | 4 | export default defineEventHandler((event) => { 5 | console.log('---------oidc nitro --------------') 6 | const config = useRuntimeConfig() 7 | const baseUrl = config.app.baseURL 8 | const query = getQuery(event) 9 | const req = event.node.req 10 | 11 | // console.log(req.headers) 12 | 13 | console.log(req.url) 14 | console.log(baseUrl) 15 | return { 16 | api: 'nuxt-openid-connect api works' 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /src/runtime/server/routes/oidc/user.ts: -------------------------------------------------------------------------------- 1 | import { getCookie, deleteCookie, defineEventHandler } from 'h3' 2 | import { initClient } from '../../../utils/issueclient' 3 | import { encrypt, decrypt } from '../../../utils/encrypt' 4 | import { setCookieInfo, setCookieTokenAndRefreshToken } from '../../../utils/utils' 5 | import { useRuntimeConfig } from '#imports' 6 | 7 | export default defineEventHandler(async (event) => { 8 | const { config, op } = useRuntimeConfig().openidConnect 9 | console.log('[USER]: oidc/user calling') 10 | 11 | const sessionid = getCookie(event, config.secret) 12 | const accesstoken = getCookie(event, config.cookiePrefix + 'access_token') 13 | const refreshToken = getCookie(event, config.cookiePrefix + 'refresh_token') 14 | const userinfoCookie = getCookie(event, config.cookiePrefix + 'user_info') 15 | const issueClient = await initClient(op, event.node.req, []) 16 | 17 | if (userinfoCookie) { 18 | console.log('userinfo:Cookie') 19 | const userInfoStr: string | undefined = await decrypt(userinfoCookie, config) 20 | return JSON.parse(userInfoStr ?? '') 21 | } else if (accesstoken) { 22 | console.log('userinfo:accesstoken') 23 | try { 24 | // load user info from oidc server. 25 | const userinfo = await issueClient.userinfo(accesstoken) 26 | // add encrypted userinfo to cookies. 27 | await setCookieInfo(event, config, userinfo) 28 | return userinfo 29 | } catch (err) { 30 | console.error('[USER]: ' + err) 31 | deleteCookie(event, config.secret) 32 | deleteCookie(event, config.cookiePrefix + 'access_token') 33 | deleteCookie(event, config.cookiePrefix + 'user_info') 34 | const cookie = config.cookie 35 | if (cookie) { 36 | for (const [key, value] of Object.entries(cookie)) { 37 | deleteCookie(event, config.cookiePrefix + key) 38 | } 39 | } 40 | return {} 41 | } 42 | } else if (refreshToken) { 43 | console.log('userinfo:refresh token') 44 | const tokenSet = await issueClient.refresh(refreshToken) 45 | // console.log('refreshed and validated tokens %j', tokenSet) 46 | // console.log('refreshed ID Token claims %j', tokenSet.claims()) 47 | if (tokenSet.access_token) { 48 | const userinfo = await issueClient.userinfo(tokenSet.access_token) 49 | setCookieTokenAndRefreshToken(event, config, tokenSet) 50 | await setCookieInfo(event, config, userinfo) 51 | return userinfo 52 | } else { 53 | return {} 54 | } 55 | // console.log('userinfo:' + userinfo) 56 | } else { 57 | console.log('[USER]: empty accesstoken for access userinfo') 58 | return {} 59 | } 60 | }) 61 | -------------------------------------------------------------------------------- /src/runtime/storage.ts: -------------------------------------------------------------------------------- 1 | import { isUnset, isSet } from './utils/utils' 2 | 3 | export type StorageOptions = { 4 | localStorage: boolean, 5 | prefix: string, 6 | ignoreExceptions: boolean 7 | } 8 | 9 | export class Storage { 10 | public options: StorageOptions 11 | private userInfoKey: string 12 | 13 | constructor(options: StorageOptions) { 14 | this.options = options 15 | this.userInfoKey = 'user' 16 | } 17 | 18 | getPrefix(): string { 19 | if (!this.options.localStorage) { 20 | throw new Error('Cannot get prefix; localStorage is off') 21 | } 22 | return this.options.prefix 23 | } 24 | 25 | setUserInfo(user: any) { 26 | if (isUnset(user)) { 27 | return 28 | } 29 | 30 | let _userValue = user 31 | if (typeof user !== 'string') { 32 | _userValue = JSON.stringify(user) 33 | } 34 | 35 | this.setLocalStorage(this.userInfoKey, _userValue) 36 | } 37 | 38 | getUserInfo(): any { 39 | if (this.isLocalStorageEnabled()) { 40 | const _user = this.getLocalStorage(this.userInfoKey) as string 41 | try { 42 | return JSON.parse(_user) 43 | } catch (err) { 44 | console.error('error in parse local user', err) 45 | } 46 | } else { 47 | return undefined 48 | } 49 | } 50 | 51 | removeUserInfo(): void { 52 | if (this.isLocalStorageEnabled()) { 53 | this.removeLocalStorage(this.userInfoKey) 54 | } 55 | } 56 | 57 | isLoggedIn(): boolean { 58 | const user = this.getUserInfo() 59 | return isSet(user) && Object.keys(user).length > 0 60 | } 61 | 62 | setLocalStorage(key: string, value: string): string | void { 63 | // Unset null, undefined 64 | if (isUnset(value)) { 65 | return this.removeLocalStorage(key) 66 | } 67 | 68 | if (!this.isLocalStorageEnabled()) { 69 | return 70 | } 71 | 72 | const _key = this.getPrefix() + key 73 | 74 | try { 75 | localStorage.setItem(_key, value) 76 | } catch (e) { 77 | if (!this.options.ignoreExceptions) { 78 | throw e 79 | } 80 | } 81 | 82 | return value 83 | } 84 | 85 | getLocalStorage(key: string): string | null { 86 | if (!this.isLocalStorageEnabled()) { 87 | return null 88 | } 89 | 90 | const _key = this.getPrefix() + key 91 | 92 | const value = localStorage.getItem(_key) 93 | 94 | return value 95 | } 96 | 97 | removeLocalStorage(key: string): void { 98 | if (!this.isLocalStorageEnabled()) { 99 | return 100 | } 101 | 102 | const _key = this.getPrefix() + key 103 | localStorage.removeItem(_key) 104 | } 105 | 106 | // origin from https://github.com/nuxt-community/auth-module 107 | isLocalStorageEnabled(): boolean { 108 | // Local Storage only exists in the browser 109 | if (!import.meta.client) { 110 | return false 111 | } 112 | 113 | // There's no great way to check if localStorage is enabled; most solutions 114 | // error out. So have to use this hacky approach :\ 115 | // https://stackoverflow.com/questions/16427636/check-if-localstorage-is-available 116 | const test = 'test' 117 | try { 118 | localStorage.setItem(test, test) 119 | localStorage.removeItem(test) 120 | return true 121 | } catch (e) { 122 | if (!this.options.ignoreExceptions) { 123 | console.warn( 124 | "[AUTH] Local storage is enabled in config, but browser doesn't" + 125 | ' support it' 126 | ) 127 | } 128 | return false 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/runtime/utils/encrypt.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../module' 2 | 3 | export const encrypt = async (text: string, config: Config) => { 4 | const KEY = config.cookieEncryptKey 5 | const IV = config.cookieEncryptIV 6 | const ALGO = config.cookieEncryptALGO 7 | const NEED_ENCRYPT = config.cookieEncrypt 8 | 9 | if (!NEED_ENCRYPT) { return text } 10 | 11 | const crypto = await import('node:crypto') 12 | const cipher = crypto.createCipheriv(ALGO, KEY, IV) 13 | let encrypted = cipher.update(text, 'utf8', 'base64') 14 | encrypted += cipher.final('base64') 15 | return encrypted 16 | } 17 | 18 | export const decrypt = async (text: string, config: Config) => { 19 | const KEY = config.cookieEncryptKey 20 | const IV = config.cookieEncryptIV 21 | const ALGO = config.cookieEncryptALGO 22 | const NEED_ENCRYPT = config.cookieEncrypt 23 | 24 | if (!text) { return } 25 | if (!NEED_ENCRYPT) { return text } 26 | const crypto = await import('node:crypto') 27 | 28 | const decipher = crypto.createDecipheriv(ALGO, KEY, IV) 29 | const decrypted = decipher.update(text, 'base64', 'utf8') 30 | return (decrypted + decipher.final('utf8')) 31 | } 32 | -------------------------------------------------------------------------------- /src/runtime/utils/issueclient.ts: -------------------------------------------------------------------------------- 1 | import { Issuer } from 'openid-client' 2 | import { OidcProvider } from '../../module' 3 | import { useRuntimeConfig } from '#imports' 4 | 5 | export const initClient = async (op: OidcProvider, req: any, redirectUris: string[]) => { 6 | const { config } = useRuntimeConfig().openidConnect 7 | const issuer = await Issuer.discover(op.issuer) 8 | // console.log('Discovered issuer %s %O', issuer.issuer, issuer.metadata) 9 | const client = new issuer.Client({ 10 | client_id: op.clientId, 11 | client_secret: op.clientSecret, 12 | redirect_uris: redirectUris, 13 | response_type: config.response_type 14 | // id_token_signed_response_alg (default "RS256") 15 | }) // => Client 16 | 17 | return client 18 | } 19 | -------------------------------------------------------------------------------- /src/runtime/utils/template.ts: -------------------------------------------------------------------------------- 1 | import { useRuntimeConfig } from '#imports' 2 | 3 | const debug = useRuntimeConfig().openidConnect.config.debug ?? false 4 | 5 | export const CBT_PAGE_TEMPATE = ` 6 | 7 | 8 | 9 | 10 | 11 |

OIDC Callback Middle Page. Loading...

12 | 13 | 21 | 22 | 23 | 24 | ` 25 | -------------------------------------------------------------------------------- /src/runtime/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { setCookie } from 'h3' 2 | import { encrypt } from './encrypt' 3 | import { useRuntimeConfig } from '#imports' 4 | 5 | export const setCookieTokenAndRefreshToken = (event: any, config: any, tokenSet: any) => { 6 | // token setting 7 | if (tokenSet && tokenSet.expires_at) { 8 | const expireDate = new Date(tokenSet.expires_at * 1000) // second to ms 9 | setCookie(event, config.cookiePrefix + 'access_token', tokenSet.access_token, { 10 | expires: expireDate, 11 | ...config.cookieFlags['access_token' as keyof typeof config.cookieFlags] 12 | }) 13 | } else { 14 | setCookie(event, config.cookiePrefix + 'access_token', tokenSet.access_token, { 15 | maxAge: config.cookieMaxAge, 16 | ...config.cookieFlags['access_token' as keyof typeof config.cookieFlags] 17 | }) 18 | } 19 | 20 | // refresh token setting 21 | if (tokenSet && tokenSet.refresh_expires_in && tokenSet.refresh_token) { 22 | setCookie(event, config.cookiePrefix + 'refresh_token', tokenSet.refresh_token, { 23 | maxAge: tokenSet.refresh_expires_in 24 | }) 25 | } else if (tokenSet && !config.hasCookieRefreshExpireDate && tokenSet.refresh_token) { 26 | const expireDate = new Date(Date.now() + config.cookieRefreshDefaultMaxAge * 1000); 27 | setCookie(event, config.cookiePrefix + 'refresh_token', tokenSet.refresh_token, { 28 | expires: expireDate 29 | }) 30 | } 31 | } 32 | 33 | export const setCookieInfo = async (event: any, config: any, userinfo: any) => { 34 | const { cookie, isCookieUserInfo } = config 35 | if (isCookieUserInfo) { 36 | for (const [key, value] of Object.entries(userinfo)) { 37 | if (cookie && Object.prototype.hasOwnProperty.call(cookie, key)) { 38 | setCookie(event, config.cookiePrefix + key, JSON.stringify(value), { 39 | maxAge: config.cookieMaxAge, 40 | ...config.cookieFlags[key as keyof typeof config.cookieFlags] 41 | }) 42 | } 43 | } 44 | try { 45 | const encryptedText = await encrypt(JSON.stringify(userinfo), config) 46 | setCookie(event, config.cookiePrefix + 'user_info', encryptedText, { ...config.cookieFlags['user_info' as keyof typeof config.cookieFlags] }) 47 | } catch (err) { 48 | console.error('encrypted userinfo error.', err) 49 | } 50 | } 51 | } 52 | 53 | export const isUnset = (o: unknown): boolean => 54 | typeof o === 'undefined' || o === null 55 | 56 | export const isSet = (o: unknown): boolean => !isUnset(o) 57 | 58 | export const getRedirectUrl = (uri: string | null | undefined, baseURL: string | undefined = undefined): string => { 59 | if (!uri) { 60 | return baseURL || '/' 61 | } 62 | const idx = uri.indexOf('?') 63 | const searchParams = new URLSearchParams(idx >= 0 ? uri.substring(idx) : uri) 64 | const redirUrl = (baseURL ? baseURL + '/' : '') + searchParams.get('redirect') 65 | const cleanUrl = getCleanUrl(redirUrl) 66 | return cleanUrl || baseURL || '/' 67 | } 68 | 69 | export function getCallbackUrl(redirectUrl: string, host: string | undefined): string { 70 | return getDefaultBackUrl(redirectUrl, host) 71 | } 72 | 73 | export function getDefaultBackUrl(redirectUrl: string, host: string | undefined): string { 74 | const config = useRuntimeConfig() 75 | const baseUrl = config.app.baseURL 76 | console.log('------>baseUrl:' + baseUrl) 77 | return getCleanUrl('http://' + host + baseUrl + '/oidc/cbt?redirect=' + redirectUrl) 78 | } 79 | 80 | export function getCleanUrl(url: string): string { 81 | return url.indexOf(':') >=0 ? url.replace(/([^:]\/)\/+/g, "$1") : url.replace(/\/\/+/g, '/') 82 | } 83 | /** 84 | * Response Mode 85 | * The Response Mode determines how the Authorization Server returns result parameters from the Authorization Endpoint. 86 | * Non-default modes are specified using the response_mode request parameter. If response_mode is not present in a request, the default Response Mode mechanism specified by the Response Type is used. 87 | * 88 | * REF: 89 | * https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html (English) 90 | * https://linianhui.github.io/authentication-and-authorization/05-openid-connect-extension/ (Chinese) 91 | * 92 | * response_mode 93 | * 1. query 94 | * In this mode, Authorization Response parameters are encoded in the query string added to the redirect_uri when redirecting back to the Client. 95 | * callback http.method: 'GET' 96 | * example: https://client.lnh.dev/oauth2-callback?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz 97 | * 2. fragment 98 | * In this mode, Authorization Response parameters are encoded in the fragment added to the redirect_uri when redirecting back to the Client. 99 | * callback http.method: 'GET' 100 | * example: http://client.lnh.dev/oauth2-callback#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&expires_in=3600 101 | * 3. form_post (Callback method 'POST') (REF: https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) 102 | * 103 | * Hints: 104 | * i) For purposes of this specification, the default Response Mode for the OAuth 2.0 code Response Type is the query encoding. 105 | * ii) For purposes of this specification, the default Response Mode for the OAuth 2.0 token Response Type is the fragment encoding. 106 | * iii) Response_mode 'query' not allowed for implicit or hybrid flow 107 | */ 108 | export function getResponseMode(config: any): string { 109 | const responseType = config.response_type 110 | return config.response_mode || getDefaultResponseMode(responseType) 111 | } 112 | 113 | function getDefaultResponseMode(responseType: string): string { 114 | const resTypeArray = responseType.match(/[^ ]+/g) 115 | if (resTypeArray && resTypeArray?.findIndex(i => i === 'code') >= 0) { 116 | return 'query' 117 | } else if (resTypeArray && resTypeArray?.findIndex(i => i === 'token')) { 118 | return 'fragment' 119 | } 120 | return 'query' 121 | } 122 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------