23 Commits

Author SHA1 Message Date
a77e689b38 Update plugin.ts 2023-06-22 21:32:28 +02:00
René Preuß
3ce7d64d50 Update CHANGELOG.md 2023-04-08 18:37:50 +02:00
René Preuß
6864194251 chore(release): v2.0.1 2023-04-08 18:33:28 +02:00
René Preuß
fc4abb27d5 Fix documentation 2023-04-08 18:33:00 +02:00
René Preuß
5571f4584e chore(release): v2.0.1 2023-04-08 18:28:34 +02:00
René Preuß
734b495ec0 Make options optional
Revert default routes
Update documentation
2023-04-08 18:26:18 +02:00
René Preuß
1560ae2038 Add UPGRADE.md 2023-04-08 16:26:32 +02:00
René Preuß
4b7d11f44d Update documentation
Fix typo
2023-04-08 16:07:39 +02:00
René Preuß
06feac925b PKCE implementation 2023-04-08 15:42:50 +02:00
René Preuß
f27a14c860 chore(release): v1.0.6 2023-04-08 12:30:13 +02:00
René Preuß
479e7d4b22 init only when used 2023-04-08 12:29:44 +02:00
René Preuß
29915ebd3b Fix ref for watch 2023-04-08 12:16:01 +02:00
René Preuß
81b48ac806 Merge branch 'main' of github.com:bitinflow/nuxt-oauth 2023-04-08 11:40:17 +02:00
René Preuß
5454c9677b chore(release): v1.0.5 2023-04-08 11:39:35 +02:00
René Preuß
434c335e3f Add access token ref to useAuth return 2023-04-08 11:38:41 +02:00
René Preuß
ebad02a1e1 chore(release): v1.0.4 2023-02-19 12:47:08 +01:00
René Preuß
693f60a306 Update README.md 2023-02-18 19:36:21 +01:00
René Preuß
0981a12d08 chore(release): v1.0.3 2023-02-18 18:57:25 +01:00
René Preuß
36ccf819bd Change endpoints.logout to nullable
Add redirect_uri in logout route
2023-02-18 18:56:57 +01:00
René Preuß
f2e4b5c1c9 chore(release): v1.0.2 2023-02-18 14:46:38 +01:00
René Preuß
e0c8c411a1 Fix route 2023-02-18 14:46:10 +01:00
René Preuß
15c3d43831 Add important documentation 2023-02-18 14:45:14 +01:00
René Preuß
c954054621 Change readme 2023-02-18 14:34:42 +01:00
12 changed files with 431 additions and 79 deletions

30
CHANGELOG.md Normal file
View File

@@ -0,0 +1,30 @@
# Changelog
## v2.0.1
Typo fixes in the GitHub/NPM repo
## v2.0.0
Support for Authorization Code Grant with PKCE
## v1.0.5 - v1.0.6
Fix for CookieRef when using watch(...)
## v1.0.4
Minor fixes
## v1.0.3
Minor fixes
## v1.0.2
Minor fixes
## v1.0.0
Initial Release

View File

@@ -1,47 +1,90 @@
# @bitinflow/nuxt-oauth # 🔒 @bitinflow/nuxt-oauth
[![npm version][npm-version-src]][npm-version-href] **@bitinflow/nuxt-oauth** is a Nuxt 3 Module that provides a simple OAuth 2 implementation for static site nuxt
[![npm downloads][npm-downloads-src]][npm-downloads-href] applications for which no backend code is required. It uses the recommended Authorization Code Grant with PKCE by
[![License][license-src]][license-href] default and supports Implicit Grant Tokens as well.
> My new Nuxt module This package is intended to be used with Laravel Passport, allowing users to interact with their first-party API using
their own OAuth provider. Currently, it does not support multiple OAuth providers. With **@bitinflow/nuxt-oauth**,
developers can quickly and easily implement secure OAuth authentication in their Nuxt applications.
- [✨  Release Notes](/CHANGELOG.md) - [✨  Release Notes](/CHANGELOG.md)
<!-- - [📖 &nbsp;Documentation](https://example.com) -->
## Features ## Features
<!-- Highlight some of the features your module provide here --> - 📦 Authorization Code Grant with PKCE (default)
- ⛰ &nbsp;Foo - 📦 Simple OAuth 2 Implicit Grant Token
- 🚠 &nbsp;Bar authentication ([not recommended](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics))
- 🌲 &nbsp;Baz - 📦 Intended to be used with laravel-passport
- 📦 Single OAuth provider support (currently)
## Quick Setup ## Quick Setup
1. Add `my-module` dependency to your project > **Note:** Starting with **@bitinflow/nuxt-oauth** v2.0.0, the default response type is `code`. If you want to use the
> `token` response type, you need to set it explicitly in the configuration.
1. Add `@bitinflow/nuxt-oauth` dependency to your project
```bash ```bash
# Using pnpm # Using pnpm
pnpm add -D my-module pnpm add -D @bitinflow/nuxt-oauth
# Using yarn # Using yarn
yarn add --dev my-module yarn add --dev @bitinflow/nuxt-oauth
# Using npm # Using npm
npm install --save-dev my-module npm install --save-dev @bitinflow/nuxt-oauth
``` ```
2. Add `my-module` to the `modules` section of `nuxt.config.ts` 2. Add `@bitinflow/nuxt-oauth` to the `modules` section of `nuxt.config.ts` and disable `ssr`.
Or alternatively disable `ssr` via `routeRules`, only for pages where `auth` or `guest` middlewares are needed.
Typically account section and login page.
```js ```js
export default defineNuxtConfig({ export default defineNuxtConfig({
modules: [ modules: [
'my-module' '@bitinflow/nuxt-oauth'
] ],
ssr: false,
// or
routeRules: {
'/dashboard/**': {ssr: false},
'/whatever/**': {ssr: false}
},
// using code response type (default)
oauth: {
endpoints: {
authorization: 'https://example.com/oauth/authorize',
token: 'https://example.com/oauth/token',
userInfo: 'https://example.com/api/users/me',
logout: 'https://example.com/oauth/logout'
},
clientId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
scope: ['user:read']
},
// using token response type (not recommended)
oauth: {
endpoints: {
authorization: 'https://example.com/oauth/authorize',
userInfo: 'https://example.com/api/users/me',
logout: 'https://example.com/oauth/logout'
},
clientId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
responseType: 'token',
scope: ['user:read']
},
}) })
``` ```
That's it! You can now use My Module in your Nuxt app ✨ This will be your callback url (host is determined by `window.location.origin`):
- Callback: `http://localhost:3000/login`
That's it! You can now use @bitinflow/nuxt-oauth in your Nuxt app ✨
## Development ## Development
@@ -68,13 +111,3 @@ npm run test:watch
# Release new version # Release new version
npm run release npm run release
``` ```
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/my-module/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/my-module
[npm-downloads-src]: https://img.shields.io/npm/dm/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-downloads-href]: https://npmjs.com/package/my-module
[license-src]: https://img.shields.io/npm/l/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://npmjs.com/package/my-module

13
UPGRADE.md Normal file
View File

@@ -0,0 +1,13 @@
# Upgrade Guide
## General Notes
## Upgrading To 2.0 From 1.x
### Changing default response type to `code`
OAuth 2 Implicit Grant Token authentication
is [not recommended](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics) anymore. If you still
want to use the `token` response type, you need to set it explicitly with `responseType: 'token'` in the
`oauth` configuration. Otherwise, you will use Authorization Code Grant with PKCE by default.

84
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@bitinflow/nuxt-oauth", "name": "@bitinflow/nuxt-oauth",
"version": "1.0.0", "version": "1.0.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@bitinflow/nuxt-oauth", "name": "@bitinflow/nuxt-oauth",
"version": "1.0.0", "version": "1.0.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nuxt/kit": "^3.2.2", "@nuxt/kit": "^3.2.2",
@@ -17,6 +17,7 @@
"@nuxt/module-builder": "^0.2.1", "@nuxt/module-builder": "^0.2.1",
"@nuxt/schema": "^3.2.2", "@nuxt/schema": "^3.2.2",
"@nuxt/test-utils": "^3.2.2", "@nuxt/test-utils": "^3.2.2",
"axios": "^1.3.5",
"changelogen": "^0.4.1", "changelogen": "^0.4.1",
"eslint": "^8.34.0", "eslint": "^8.34.0",
"nuxt": "^3.2.2", "nuxt": "^3.2.2",
@@ -2639,6 +2640,12 @@
"integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==",
"dev": true "dev": true
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.13", "version": "10.4.13",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz",
@@ -2672,6 +2679,17 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"node_modules/axios": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz",
"integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3338,6 +3356,18 @@
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ=="
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -3703,6 +3733,15 @@
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.2.tgz", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.2.tgz",
"integrity": "sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==" "integrity": "sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ=="
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": { "node_modules/delegates": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -4508,6 +4547,20 @@
} }
} }
}, },
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": { "node_modules/formdata-polyfill": {
"version": "4.0.10", "version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -5862,6 +5915,27 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": { "node_modules/mimic-fn": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@@ -7337,6 +7411,12 @@
"integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==",
"dev": true "dev": true
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/prr": { "node_modules/prr": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bitinflow/nuxt-oauth", "name": "@bitinflow/nuxt-oauth",
"version": "1.0.1", "version": "2.0.1",
"description": "Nuxt 3 OAuth Module", "description": "Nuxt 3 OAuth Module",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
@@ -21,7 +21,8 @@
"dev": "nuxi dev playground", "dev": "nuxi dev playground",
"dev:build": "nuxi build playground", "dev:build": "nuxi build playground",
"dev:prepare": "nuxt-module-build --stub && nuxi prepare playground", "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground",
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish --access public && git push --follow-tags", "release": "npm run lint && npm run test && npm run prepack && changelogen --release && git push --follow-tags",
"push": "npm publish --access public",
"lint": "eslint .", "lint": "eslint .",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest watch" "test:watch": "vitest watch"
@@ -35,6 +36,7 @@
"@nuxt/module-builder": "^0.2.1", "@nuxt/module-builder": "^0.2.1",
"@nuxt/schema": "^3.2.2", "@nuxt/schema": "^3.2.2",
"@nuxt/test-utils": "^3.2.2", "@nuxt/test-utils": "^3.2.2",
"axios": "^1.3.5",
"changelogen": "^0.4.1", "changelogen": "^0.4.1",
"eslint": "^8.34.0", "eslint": "^8.34.0",
"nuxt": "^3.2.2", "nuxt": "^3.2.2",

View File

@@ -1,18 +1,13 @@
export default defineNuxtConfig({ export default defineNuxtConfig({
modules: ['../src/module'], modules: ['../src/module'],
ssr: false,
oauth: { oauth: {
redirect: { redirect: {
login: '/login/', // sandbox appends / at the end of url
logout: '/',
callback: '/login/', // sandbox appends / at the end of url
home: '/home' home: '/home'
}, },
endpoints: { clientId: '98e1cb74-125a-4d60-b686-02c2f0c87521',
authorization: 'https://api.sandbox.own3d.pro/v1/oauth/authorization', scope: ['user:read']
userInfo: `https://id.stream.tv/api/users/@me`,
logout: 'https://id.stream.tv/oauth/token'
},
clientId: '90a951d1-ea50-4fda-8c4d-275b81f7d219',
scope: ['user:read', 'connections']
}, },
}) })

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import {useAuth} from "#imports"; import {useAuth, useNuxtApp} from "#imports";
const {user, signOut} = await useAuth(); const {user, signOut} = await useAuth();
@@ -7,11 +7,20 @@ definePageMeta({
middleware: ["auth"] middleware: ["auth"]
}) })
const { $api } = useNuxtApp()
$api.get('user')
.then((response: any) => {
console.log(response.data)
})
.catch((error: any) => {
console.log(error)
})
</script> </script>
<template> <template>
<div v-if="user"> <div v-if="user">
Hello {{ user.name }} Hello {{ user.data.first_name }}
<button @click="signOut"> <button @click="signOut">
Sign Out Sign Out

View File

@@ -0,0 +1,28 @@
import axios from "axios";
import {useAuth} from "#imports";
import {defineNuxtPlugin} from '#app';
import {watch} from 'vue';
export default defineNuxtPlugin(async () => {
const {bearerToken, accessToken} = await useAuth();
const api = axios.create({
baseURL: 'https://accounts.bitinflow.com/api/v3/',
headers: {
common: {
'Authorization': bearerToken(),
},
},
});
watch(accessToken, () => {
console.log('access token rotated')
api.defaults.headers.common['Authorization'] = bearerToken();
});
return {
provide: {
api: api,
},
};
});

View File

@@ -3,19 +3,25 @@ import defu from "defu";
// Module options TypeScript interface definition // Module options TypeScript interface definition
export interface ModuleOptions { export interface ModuleOptions {
redirect: { redirect?: {
login: string, login?: string,
logout: string, logout?: string,
callback: string, callback?: string,
home: string home?: string
}, },
endpoints: { endpoints?: {
authorization: string, authorization?: string,
userInfo: string, token?: string,
logout: string userInfo?: string,
logout?: string | null
}, },
clientId: string, refreshToken?: {
scope: string[] maxAge: number,
}
clientId?: string,
responseType?: 'token' | 'code',
prompt?: '' | 'none' | 'login' | 'consent',
scope?: string[]
} }
const defaults: ModuleOptions = { const defaults: ModuleOptions = {
@@ -27,11 +33,17 @@ const defaults: ModuleOptions = {
}, },
endpoints: { endpoints: {
authorization: 'https://accounts.bitinflow.com/oauth/authorize', authorization: 'https://accounts.bitinflow.com/oauth/authorize',
userInfo: `https://accounts.bitinflow.com/api/v3/user`, token: 'https://accounts.bitinflow.com/oauth/token',
logout: 'https://accounts.bitinflow.com/logout' userInfo: 'https://accounts.bitinflow.com/api/v3/user',
logout: null,
},
refreshToken: {
maxAge: 60 * 60 * 24 * 30,
}, },
clientId: 'please-set-client-id', clientId: 'please-set-client-id',
scope: ['user:read'] responseType: 'code',
prompt: '',
scope: []
} }
export default defineNuxtModule<ModuleOptions>({ export default defineNuxtModule<ModuleOptions>({
@@ -40,7 +52,7 @@ export default defineNuxtModule<ModuleOptions>({
configKey: 'oauth' configKey: 'oauth'
}, },
defaults, defaults,
setup (moduleOptions, nuxt) { setup(moduleOptions, nuxt) {
const resolver = createResolver(import.meta.url) const resolver = createResolver(import.meta.url)
const options = defu(moduleOptions, { const options = defu(moduleOptions, {
@@ -48,7 +60,7 @@ export default defineNuxtModule<ModuleOptions>({
}) })
// Set up runtime configuration // Set up runtime configuration
nuxt.options.runtimeConfig = nuxt.options.runtimeConfig || { public: {} } nuxt.options.runtimeConfig = nuxt.options.runtimeConfig || {public: {}}
nuxt.options.runtimeConfig.oauth = defu(nuxt.options.runtimeConfig.oauth, { nuxt.options.runtimeConfig.oauth = defu(nuxt.options.runtimeConfig.oauth, {
...options ...options
}) })

View File

@@ -1,18 +1,24 @@
import {CookieRef, navigateTo, useCookie, useRuntimeConfig} from "#app"; import {CookieRef, navigateTo, useCookie, useRuntimeConfig} from "#app";
import {ModuleOptions} from "../../module"; import {ModuleOptions} from "../../module";
import {generateRandomString, getChallengeFromVerifier} from "../support";
declare interface ComposableOptions { declare interface ComposableOptions {
fetchUserOnInitialization: boolean fetchUserOnInitialization: boolean
} }
let user: CookieRef<any>;
let accessToken: CookieRef<any>;
let refreshToken: CookieRef<any>;
export default async (options: ComposableOptions = { export default async (options: ComposableOptions = {
fetchUserOnInitialization: false fetchUserOnInitialization: false
}) => { }) => {
const user: CookieRef<any> = useCookie('oauth_user')
const accessToken: CookieRef<any> = useCookie('oauth_access_token')
const authConfig = useRuntimeConfig().public.oauth as ModuleOptions; const authConfig = useRuntimeConfig().public.oauth as ModuleOptions;
if (user == null) user = useCookie('oauth_user');
if (accessToken == null) accessToken = useCookie('oauth_access_token');
if (refreshToken == null) refreshToken = useCookie('oauth_refresh_token');
const fetchUser = async () => { const fetchUser = async (): Promise<void> => {
try { try {
const response = await fetch(authConfig.endpoints.userInfo, { const response = await fetch(authConfig.endpoints.userInfo, {
headers: { headers: {
@@ -29,15 +35,28 @@ export default async (options: ComposableOptions = {
} }
} }
const signIn = async () => { const signIn = async (): Promise<void> => {
const state = useCookie<string>('oauth_state');
state.value = generateRandomString();
// create oauth authorization url // create oauth authorization url
const params = new URLSearchParams({ const params = new URLSearchParams({
client_id: authConfig.clientId, client_id: authConfig.clientId,
redirect_uri: window.location.origin + authConfig.redirect.callback, redirect_uri: window.location.origin + authConfig.redirect.callback,
response_type: 'token', response_type: authConfig.responseType,
scope: authConfig.scope.join(' ') scope: authConfig.scope.join(' '),
state: state.value,
prompt: authConfig.prompt
}) })
if (authConfig.responseType === 'code') {
const codeVerifier = useCookie<string>('oauth_code_verifier');
codeVerifier.value = generateRandomString();
params.set('code_challenge', await getChallengeFromVerifier(codeVerifier.value))
params.set('code_challenge_method', 'S256')
}
window.location.href = `${authConfig.endpoints.authorization}?${params.toString()}` window.location.href = `${authConfig.endpoints.authorization}?${params.toString()}`
}; };
@@ -45,24 +64,48 @@ export default async (options: ComposableOptions = {
accessToken.value = null; accessToken.value = null;
user.value = null; user.value = null;
return navigateTo('/') if (authConfig.endpoints.logout) {
// create oauth logout url
const params = new URLSearchParams({
client_id: authConfig.clientId,
redirect_uri: window.location.origin + authConfig.redirect.logout
})
window.location.href = `${authConfig.endpoints.logout}?${params.toString()}`
}
return navigateTo(authConfig.redirect.logout)
} }
const setBearer = async (token: string, tokenType: string, expires: number) => { const setBearerToken = async (token: string, tokenType: string, expires: number): Promise<void> => {
accessToken.value = {token, tokenType, expiresAt: Date.now() + expires * 1000}; accessToken.value = {token, tokenType, expiresAt: Date.now() + expires * 1000};
await fetchUser() await fetchUser()
} }
const setRefreshToken = async (token: string, tokenType: string, expires: number): Promise<void> => {
refreshToken.value = {token, tokenType, expiresAt: Date.now() + expires * 1000};
}
// Initialize the user if the option is set to true // Initialize the user if the option is set to true
if (options.fetchUserOnInitialization) { if (options.fetchUserOnInitialization) {
await fetchUser() await fetchUser()
} }
const bearerToken = () => {
return accessToken.value
? `${accessToken.value.tokenType} ${accessToken.value.token}`
: null;
}
return { return {
user, user,
signIn, signIn,
signOut, signOut,
setBearer, setBearerToken,
setRefreshToken,
bearerToken,
accessToken,
refreshToken,
authConfig authConfig
} }
} }

View File

@@ -1,20 +1,91 @@
import {addRouteMiddleware, defineNuxtPlugin, navigateTo} from '#app' import {addRouteMiddleware, defineNuxtPlugin, navigateTo, useCookie} from '#app'
import useAuth from "./composables/useAuth" import useAuth from "./composables/useAuth"
import {RouteLocationNormalized} from "vue-router";
import {ModuleOptions} from "../module";
interface AccessToken {
access_token: string,
token_type: string,
expires_in: number,
refresh_token: string
scope: string
}
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const resolveUsingToken = async (
to: RouteLocationNormalized,
authConfig: ModuleOptions,
setBearerToken: (token: string, tokenType: string, expires: number) => Promise<void>
) => {
const hashParams = new URLSearchParams(to.hash.substring(1))
if (hashParams.has('access_token')) {
const token = hashParams.get('access_token') as string;
const tokenType = hashParams.get('token_type') as string;
const expires = hashParams.get('expires_in') as string;
await setBearerToken(token, tokenType, parseInt(expires));
return navigateTo(authConfig.redirect.home)
}
}
const resolveUsingCode = async (
to: RouteLocationNormalized,
authConfig: ModuleOptions,
setBearerToken: (token: string, tokenType: string, expires: number) => Promise<void>,
setRefreshToken: (token: string, tokenType: string, expires: number) => Promise<void>
) => {
if (to.query['code']) {
const code = to.query['code'] as string;
const stateFromRequest = to.query['state'] as string;
const stateFromCookie = useCookie<string>('oauth_state');
const codeVerifier = useCookie<string>('oauth_code_verifier');
if (stateFromRequest !== stateFromCookie.value) {
console.warn('State mismatch', stateFromRequest, stateFromCookie.value)
return navigateTo(authConfig.redirect.login)
}
const formData = new FormData();
formData.append('grant_type', 'authorization_code')
formData.append('client_id', authConfig.clientId)
formData.append('redirect_uri', window.location.origin + authConfig.redirect.callback)
formData.append('code_verifier', codeVerifier.value)
formData.append('code', code)
const response: Response = await fetch(authConfig.endpoints.token, {
method: 'POST',
body: formData,
})
if (!response.ok) {
console.warn('Failed to fetch token', response)
return navigateTo(authConfig.redirect.login)
}
const data: AccessToken = await response.json();
await setBearerToken(data.access_token, data.token_type, data.expires_in)
await setRefreshToken(data.refresh_token, data.token_type, authConfig.refreshToken.maxAge)
return navigateTo(authConfig.redirect.home)
}
}
addRouteMiddleware('auth', async (to) => { addRouteMiddleware('auth', async (to) => {
const {user, authConfig, setBearer} = await useAuth() const {user, authConfig, setBearerToken, setRefreshToken} = await useAuth()
if (to.path === authConfig.redirect.callback) { if (to.path === authConfig.redirect.callback || to.path === authConfig.redirect.callback + '/') {
const params = new URLSearchParams(to.hash.substring(1)) const queryParams = new URLSearchParams(to.query.toString());
if (queryParams.has('error')) {
return navigateTo(authConfig.redirect.login)
}
if (params.has('access_token')) { if (authConfig.responseType === 'token') {
const token = params.get('access_token') as string; return await resolveUsingToken(to, authConfig, setBearerToken)
const tokenType = params.get('token_type') as string; }
const expires = params.get('expires_in') as string;
await setBearer(token, tokenType, parseInt(expires)); if (authConfig.responseType === 'code') {
return navigateTo(authConfig.redirect.home) return await resolveUsingCode(to, authConfig, setBearerToken, setRefreshToken)
} }
return return

36
src/runtime/support.ts Normal file
View File

@@ -0,0 +1,36 @@
/*
* Source: https://docs.cotter.app/sdk-reference/api-for-other-mobile-apps/api-for-mobile-apps
*/
function dec2hex(dec: any) {
return ('0' + dec.toString(16)).substr(-2)
}
export function generateRandomString() {
const array = new Uint32Array(56 / 2);
window.crypto.getRandomValues(array);
return Array.from(array, dec2hex).join('');
}
function sha256(plain: any) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
function base64urlencode(a: any) {
let str = "";
const bytes = new Uint8Array(a);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
str += String.fromCharCode(bytes[i]);
}
return btoa(str)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
export async function getChallengeFromVerifier(v: any) {
return base64urlencode(await sha256(v));
}