mirror of
https://github.com/bitinflow/nuxt-oauth.git
synced 2026-03-13 13:45:59 +00:00
Compare commits
27 Commits
v1.0.5
...
cookie-opt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
761960dc8a | ||
|
|
6c2d6630ac | ||
|
|
1c05acbcdd | ||
|
|
ef07ddb80e | ||
|
|
0b3479ac01 | ||
|
|
b80604529c | ||
|
|
3ed1fd729a | ||
|
|
197a1a5c8d | ||
|
|
df53718392 | ||
|
|
ce2ced4ffa | ||
|
|
7384a4c5ce | ||
|
|
eff160b3c5 | ||
|
|
464e72535b | ||
| a77e689b38 | |||
|
|
3ce7d64d50 | ||
|
|
6864194251 | ||
|
|
fc4abb27d5 | ||
|
|
5571f4584e | ||
|
|
734b495ec0 | ||
|
|
1560ae2038 | ||
|
|
4b7d11f44d | ||
|
|
06feac925b | ||
|
|
f27a14c860 | ||
|
|
479e7d4b22 | ||
|
|
29915ebd3b | ||
|
|
81b48ac806 | ||
|
|
693f60a306 |
22
.github/workflows/package.yml
vendored
Normal file
22
.github/workflows/package.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Node.js Package
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '18.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
scope: '@bitinflow'
|
||||||
|
- run: yarn install
|
||||||
|
- run: yarn ci
|
||||||
|
- run: yarn publish
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -1,14 +1,41 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.0.4
|
||||||
|
|
||||||
## v1.0.5
|
Add `cookies` option in `ModuleOptions` interface
|
||||||
|
|
||||||
|
## v2.0.3
|
||||||
|
|
||||||
|
Add ", { external: true }" in navigateTo
|
||||||
|
|
||||||
|
## v2.0.2
|
||||||
|
|
||||||
|
Fix for `auth` middleware
|
||||||
|
|
||||||
|
## 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
|
## v1.0.4
|
||||||
|
|
||||||
|
Minor fixes
|
||||||
|
|
||||||
## v1.0.3
|
## v1.0.3
|
||||||
|
|
||||||
|
Minor fixes
|
||||||
|
|
||||||
## v1.0.2
|
## v1.0.2
|
||||||
|
|
||||||
|
Minor fixes
|
||||||
|
|
||||||
## v1.0.0
|
## v1.0.0
|
||||||
|
|
||||||
Initial Release
|
Initial Release
|
||||||
|
|||||||
121
README.md
121
README.md
@@ -1,17 +1,28 @@
|
|||||||
# @bitinflow/nuxt-oauth
|
# 🔒 @bitinflow/nuxt-oauth
|
||||||
|
|
||||||
> Nuxt module for OAuth2 authentication
|
**@bitinflow/nuxt-oauth** is a Nuxt 3 Module that provides a simple OAuth 2 implementation for static site nuxt
|
||||||
|
applications for which no backend code is required. It uses the recommended Authorization Code Grant with PKCE by
|
||||||
|
default and supports Implicit Grant Tokens as well.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 📦 OAuth2 authentication
|
- 📦 Authorization Code Grant with PKCE (default)
|
||||||
- 📦 Supports only one OAuth2 provider
|
- 📦 Simple OAuth 2 Implicit Grant Token
|
||||||
- 📦 Supports only implicit flow
|
authentication ([not recommended](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics))
|
||||||
|
- 📦 Intended to be used with laravel-passport
|
||||||
|
- 📦 Single OAuth provider support (currently)
|
||||||
|
|
||||||
## Quick Setup
|
## Quick Setup
|
||||||
|
|
||||||
|
> **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
|
1. Add `@bitinflow/nuxt-oauth` dependency to your project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -26,8 +37,9 @@ npm install --save-dev @bitinflow/nuxt-oauth
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. Add `@bitinflow/nuxt-oauth` to the `modules` section of `nuxt.config.ts` and disable `ssr`.
|
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.
|
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({
|
||||||
@@ -38,30 +50,105 @@ export default defineNuxtConfig({
|
|||||||
ssr: false,
|
ssr: false,
|
||||||
// or
|
// or
|
||||||
routeRules: {
|
routeRules: {
|
||||||
'/account/**': { ssr: false },
|
'/dashboard/**': {ssr: false},
|
||||||
'/auth/**': { ssr: false }
|
'/whatever/**': {ssr: false}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// example 1: using code response type (default)
|
||||||
oauth: {
|
oauth: {
|
||||||
redirect: {
|
|
||||||
login: '/login',
|
|
||||||
logout: '/',
|
|
||||||
callback: '/login',
|
|
||||||
home: '/home'
|
|
||||||
},
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
authorization: 'https://example.com/v1/oauth/authorization',
|
authorization: 'https://example.com/oauth/authorize',
|
||||||
userInfo: `https://example.com/api/users/me`,
|
token: 'https://example.com/oauth/token',
|
||||||
|
userInfo: 'https://example.com/api/users/me',
|
||||||
logout: 'https://example.com/oauth/logout'
|
logout: 'https://example.com/oauth/logout'
|
||||||
},
|
},
|
||||||
clientId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
clientId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
||||||
scope: ['user:read']
|
scope: ['user:read']
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// example 2: 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']
|
||||||
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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 ✨
|
That's it! You can now use @bitinflow/nuxt-oauth in your Nuxt app ✨
|
||||||
|
|
||||||
|
## Module Options
|
||||||
|
|
||||||
|
The module provides a set of customizable options to configure OAuth-based authentication for your application. Below is a detailed description of each option and its default values:
|
||||||
|
|
||||||
|
### `redirect`
|
||||||
|
|
||||||
|
This option defines the URLs for redirection during the authentication process.
|
||||||
|
|
||||||
|
- `login` (`string`): The URL to redirect to when a user needs to log in. Default: `/login`.
|
||||||
|
- `logout` (`string`): The URL to redirect to after logging out. Default: `/`.
|
||||||
|
- `callback` (`string`): The URL to handle the OAuth callback. Default: `/login`.
|
||||||
|
- `home` (`string`): The URL to redirect to after successful authentication. Default: `/`.
|
||||||
|
|
||||||
|
### `endpoints`
|
||||||
|
|
||||||
|
Configures the OAuth server endpoints for authorization, token exchange, and user information retrieval.
|
||||||
|
|
||||||
|
- `authorization` (`string`): The OAuth authorization endpoint. Default: `https://example.com/oauth/authorize`.
|
||||||
|
- `token` (`string`): The OAuth token endpoint. Default: `https://example.com/oauth/token`.
|
||||||
|
- `userInfo` (`string`): The endpoint to retrieve user information. Default: `https://example.com/api/users/me`.
|
||||||
|
- `logout` (`string | null`): The endpoint for logging out from the OAuth provider. Default: `null`.
|
||||||
|
|
||||||
|
### `refreshToken`
|
||||||
|
|
||||||
|
Manages the refresh token settings.
|
||||||
|
|
||||||
|
- `maxAge` (`number`): The maximum age (in seconds) for storing the refresh token in cookies. Default: `60 * 60 * 24 * 30` (30 days).
|
||||||
|
|
||||||
|
### `cookies`
|
||||||
|
|
||||||
|
Configures cookie settings for storing OAuth tokens and related data.
|
||||||
|
|
||||||
|
- `prefix` (`string`): A prefix for all cookie names. Default: none.
|
||||||
|
- `names`: Specific names for different OAuth-related cookies.
|
||||||
|
- `oauth_user`: The cookie name for storing the OAuth user. Default: `oauth_user`.
|
||||||
|
- `oauth_state`: The cookie name for storing the OAuth state. Default: `oauth_state`.
|
||||||
|
- `oauth_code_verifier`: The cookie name for storing the OAuth code verifier. Default: `oauth_code_verifier`.
|
||||||
|
- `oauth_access_token`: The cookie name for storing the access token. Default: `oauth_access_token`.
|
||||||
|
- `oauth_refresh_token`: The cookie name for storing the refresh token. Default: `oauth_refresh_token`.
|
||||||
|
- `options`: Additional settings for cookie behavior.
|
||||||
|
- `path` (`string`): The cookie path. Default: none.
|
||||||
|
- `maxAge` (`number`): The cookie's maximum age (in seconds). Default: none.
|
||||||
|
- `secure` (`boolean`): Whether the cookie should only be sent over HTTPS. Default: none.
|
||||||
|
- `sameSite` (`string`): Sets the `SameSite` cookie attribute (`lax`, `strict`, or `none`). Default: none.
|
||||||
|
- `domain` (`string`): Specifies the cookie's domain. Default: none.
|
||||||
|
- `httpOnly` (`boolean`): Indicates if the cookie is inaccessible to JavaScript. Default: none.
|
||||||
|
|
||||||
|
### `clientId`
|
||||||
|
|
||||||
|
- (`string`): The client ID used for OAuth authentication. Default: `please-set-client-id`.
|
||||||
|
|
||||||
|
### `responseType`
|
||||||
|
|
||||||
|
- (`'token' | 'code'`): The type of OAuth response, either token-based or code-based flow. Default: `code`.
|
||||||
|
|
||||||
|
### `prompt`
|
||||||
|
|
||||||
|
- (`'' | 'none' | 'login' | 'consent'`): The prompt parameter to control the OAuth flow. Default: `''`.
|
||||||
|
|
||||||
|
### `scope`
|
||||||
|
|
||||||
|
- (`string[]`): The OAuth scopes requested during authentication. Default: `[]` (empty array).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
13
UPGRADE.md
Normal file
13
UPGRADE.md
Normal 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.
|
||||||
|
|
||||||
9892
package-lock.json
generated
9892
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@bitinflow/nuxt-oauth",
|
"name": "@bitinflow/nuxt-oauth",
|
||||||
"version": "1.0.5",
|
"version": "2.0.4",
|
||||||
"description": "Nuxt 3 OAuth Module",
|
"description": "Nuxt 3 OAuth Module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -21,7 +21,9 @@
|
|||||||
"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",
|
"ci": "npm run lint && npm run dev:prepare && npm run test && npm run prepack",
|
||||||
|
"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 +37,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.6.0",
|
||||||
"changelogen": "^0.4.1",
|
"changelogen": "^0.4.1",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"nuxt": "^3.2.2",
|
"nuxt": "^3.2.2",
|
||||||
|
|||||||
@@ -5,17 +5,14 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
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: {
|
cookies: {
|
||||||
authorization: 'https://api.sandbox.own3d.pro/v1/oauth/authorization',
|
options: {
|
||||||
userInfo: `https://id.stream.tv/api/users/@me`,
|
domain: '*.own3d.pro'
|
||||||
logout: 'https://id.stream.tv/oauth/token'
|
}
|
||||||
},
|
},
|
||||||
clientId: '90a951d1-ea50-4fda-8c4d-275b81f7d219',
|
clientId: '98e1cb74-125a-4d60-b686-02c2f0c87521',
|
||||||
scope: ['user:read', 'connections']
|
scope: ['user:read']
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
28
playground/plugins/axios.ts
Normal file
28
playground/plugins/axios.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -3,19 +3,43 @@ 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 | null
|
userInfo?: string,
|
||||||
|
logout?: string | null
|
||||||
},
|
},
|
||||||
clientId: string,
|
refreshToken?: {
|
||||||
scope: string[]
|
maxAge: number,
|
||||||
|
}
|
||||||
|
cookies?: {
|
||||||
|
prefix?: string,
|
||||||
|
names?: {
|
||||||
|
oauth_user?: 'oauth_user'
|
||||||
|
oauth_state?: 'oauth_state'
|
||||||
|
oauth_code_verifier?: 'oauth_code_verifier'
|
||||||
|
oauth_access_token?: 'oauth_access_token'
|
||||||
|
oauth_refresh_token?: 'oauth_refresh_token'
|
||||||
|
}
|
||||||
|
options?: {
|
||||||
|
path?: string,
|
||||||
|
maxAge?: number,
|
||||||
|
secure?: boolean,
|
||||||
|
sameSite?: string,
|
||||||
|
domain?: string,
|
||||||
|
httpOnly?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clientId?: string,
|
||||||
|
responseType?: 'token' | 'code',
|
||||||
|
prompt?: '' | 'none' | 'login' | 'consent',
|
||||||
|
scope?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults: ModuleOptions = {
|
const defaults: ModuleOptions = {
|
||||||
@@ -26,12 +50,18 @@ const defaults: ModuleOptions = {
|
|||||||
home: '/'
|
home: '/'
|
||||||
},
|
},
|
||||||
endpoints: {
|
endpoints: {
|
||||||
authorization: 'https://accounts.bitinflow.com/oauth/authorize',
|
authorization: 'https://example.com/oauth/authorize',
|
||||||
userInfo: `https://accounts.bitinflow.com/api/v3/user`,
|
token: 'https://example.com/oauth/token',
|
||||||
|
userInfo: 'https://example.com/api/users/me',
|
||||||
logout: null,
|
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 +70,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 +78,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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import {CookieRef, navigateTo, useCookie, useRuntimeConfig} from "#app";
|
import {CookieRef, navigateTo, useRuntimeConfig} from "#app";
|
||||||
import {ModuleOptions} from "../../module";
|
import {ModuleOptions} from "../../module";
|
||||||
|
import { generateRandomString, getChallengeFromVerifier, useCookie } 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', authConfig);
|
||||||
|
if (accessToken == null) accessToken = useCookie('oauth_access_token', authConfig);
|
||||||
|
if (refreshToken == null) refreshToken = useCookie('oauth_refresh_token', authConfig);
|
||||||
|
|
||||||
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', authConfig);
|
||||||
|
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', authConfig);
|
||||||
|
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()}`
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,14 +74,18 @@ export default async (options: ComposableOptions = {
|
|||||||
window.location.href = `${authConfig.endpoints.logout}?${params.toString()}`
|
window.location.href = `${authConfig.endpoints.logout}?${params.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return navigateTo(authConfig.redirect.logout)
|
return navigateTo(authConfig.redirect.logout, { external: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const setBearerToken = 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()
|
||||||
@@ -79,8 +102,10 @@ export default async (options: ComposableOptions = {
|
|||||||
signIn,
|
signIn,
|
||||||
signOut,
|
signOut,
|
||||||
setBearerToken,
|
setBearerToken,
|
||||||
|
setRefreshToken,
|
||||||
bearerToken,
|
bearerToken,
|
||||||
accessToken,
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
authConfig
|
authConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,100 @@
|
|||||||
import {addRouteMiddleware, defineNuxtPlugin, navigateTo} from '#app'
|
import {addRouteMiddleware, defineNuxtPlugin, navigateTo} from '#app'
|
||||||
import useAuth from "./composables/useAuth"
|
import useAuth from "./composables/useAuth"
|
||||||
|
import {RouteLocationNormalized} from "vue-router";
|
||||||
|
import {ModuleOptions} from "../module";
|
||||||
|
import {useCookie} from "./support";
|
||||||
|
|
||||||
|
interface AccessToken {
|
||||||
|
access_token: string,
|
||||||
|
token_type: string,
|
||||||
|
expires_in: number,
|
||||||
|
refresh_token: string
|
||||||
|
scope: string
|
||||||
|
}
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
addRouteMiddleware('auth', async (to) => {
|
const resolveUsingToken = async (
|
||||||
const {user, authConfig, setBearerToken} = await useAuth()
|
to: RouteLocationNormalized,
|
||||||
|
authConfig: ModuleOptions,
|
||||||
|
setBearerToken: (token: string, tokenType: string, expires: number) => Promise<void>
|
||||||
|
) => {
|
||||||
|
const hashParams = new URLSearchParams(to.hash.substring(1))
|
||||||
|
|
||||||
if (to.path === authConfig.redirect.callback) {
|
if (hashParams.has('access_token')) {
|
||||||
const queryParams = new URLSearchParams(to.query.toString());
|
const token = hashParams.get('access_token') as string;
|
||||||
if (queryParams.has('error')) {
|
const tokenType = hashParams.get('token_type') as string;
|
||||||
return navigateTo(authConfig.redirect.login)
|
const expires = hashParams.get('expires_in') as string;
|
||||||
|
|
||||||
|
await setBearerToken(token, tokenType, parseInt(expires));
|
||||||
|
return navigateTo(authConfig.redirect.home, { external: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
console.log('options', authConfig.cookies?.options)
|
||||||
|
const stateFromCookie = useCookie<string>('oauth_state', authConfig);
|
||||||
|
const codeVerifier = useCookie<string>('oauth_code_verifier', authConfig);
|
||||||
|
|
||||||
|
if (stateFromRequest !== stateFromCookie.value) {
|
||||||
|
console.warn('State mismatch', stateFromRequest, stateFromCookie.value)
|
||||||
|
return navigateTo(authConfig.redirect.login, { external: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashParams = new URLSearchParams(to.hash.substring(1))
|
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)
|
||||||
|
|
||||||
if (hashParams.has('access_token')) {
|
const response: Response = await fetch(authConfig.endpoints.token, {
|
||||||
const token = hashParams.get('access_token') as string;
|
method: 'POST',
|
||||||
const tokenType = hashParams.get('token_type') as string;
|
body: formData,
|
||||||
const expires = hashParams.get('expires_in') as string;
|
})
|
||||||
|
|
||||||
await setBearerToken(token, tokenType, parseInt(expires));
|
if (!response.ok) {
|
||||||
return navigateTo(authConfig.redirect.home)
|
console.warn('Failed to fetch token', response)
|
||||||
|
return navigateTo(authConfig.redirect.login, { external: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
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, { external: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addRouteMiddleware('auth', async (to) => {
|
||||||
|
const {user, authConfig, setBearerToken, setRefreshToken} = await useAuth()
|
||||||
|
|
||||||
|
if (to.path === authConfig.redirect.callback || to.path === authConfig.redirect.callback + '/') {
|
||||||
|
const queryParams = new URLSearchParams(to.query.toString());
|
||||||
|
if (queryParams.has('error')) {
|
||||||
|
return navigateTo(authConfig.redirect.login, { external: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authConfig.responseType === 'token') {
|
||||||
|
return await resolveUsingToken(to, authConfig, setBearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authConfig.responseType === 'code') {
|
||||||
|
return await resolveUsingCode(to, authConfig, setBearerToken, setRefreshToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.value === undefined) {
|
if (user.value === undefined) {
|
||||||
return navigateTo(authConfig.redirect.login)
|
return navigateTo(authConfig.redirect.login, { external: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -34,7 +102,7 @@ export default defineNuxtPlugin(() => {
|
|||||||
const {user, authConfig} = await useAuth()
|
const {user, authConfig} = await useAuth()
|
||||||
|
|
||||||
if (user.value !== undefined) {
|
if (user.value !== undefined) {
|
||||||
return navigateTo(authConfig.redirect.home)
|
return navigateTo(authConfig.redirect.home, { external: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
46
src/runtime/support.ts
Normal file
46
src/runtime/support.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Source: https://docs.cotter.app/sdk-reference/api-for-other-mobile-apps/api-for-mobile-apps
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ModuleOptions } from '../module'
|
||||||
|
import {useCookie as _useCookie, CookieRef} from "#app";
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCookie<T = string | null | undefined>(name: string, options: ModuleOptions): CookieRef<T> {
|
||||||
|
const cookieName = options.cookies?.names?.[name] || name;
|
||||||
|
const cookiePrefix = options.cookies?.prefix || '';
|
||||||
|
const cookieOptions = options.cookies?.options || {};
|
||||||
|
return _useCookie(cookiePrefix + cookieName, cookieOptions)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user