37 Commits
v1.0.1 ... main

Author SHA1 Message Date
René Preuß
1a0895cf58 Merge pull request #3 from bitinflow/cookie-options
Add cookie options
2024-10-04 10:13:56 +02:00
René Preuß
761960dc8a Bump version 2024-10-04 10:08:11 +02:00
René Preuß
6c2d6630ac Update CHANGELOG.md 2024-10-04 10:06:28 +02:00
René Preuß
1c05acbcdd Add cookie options
Bump axios version
Change default urls to example.com
Update documentation
2024-10-04 10:00:48 +02:00
René Preuß
ef07ddb80e Change ci command 2023-09-12 11:24:50 +02:00
René Preuß
0b3479ac01 Change ci command 2023-09-12 11:22:43 +02:00
René Preuß
b80604529c Change to yarn 2023-09-12 11:17:10 +02:00
René Preuß
3ed1fd729a Merge pull request #2 from bitinflow/ghostzero-patch-1
Update package.yml
2023-09-12 11:13:15 +02:00
René Preuß
197a1a5c8d Update package.yml 2023-09-12 11:13:06 +02:00
René Preuß
df53718392 Change node version 2023-09-12 11:11:03 +02:00
René Preuß
ce2ced4ffa Bump version / add ci 2023-09-12 11:03:56 +02:00
René Preuß
7384a4c5ce Allow external urls for oauth redirects 2023-09-12 10:54:18 +02:00
René Preuß
eff160b3c5 chore(release): v2.0.2 2023-06-22 21:45:59 +02:00
René Preuß
464e72535b Merge pull request #1 from bitinflow/envoyr-patch-1
Allow directory slash for login callback url
2023-06-22 21:43:39 +02:00
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
14 changed files with 6081 additions and 9971 deletions

22
.github/workflows/package.yml vendored Normal file
View 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 }}

41
CHANGELOG.md Normal file
View File

@@ -0,0 +1,41 @@
# Changelog
## v2.0.4
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
Minor fixes
## v1.0.3
Minor fixes
## v1.0.2
Minor fixes
## v1.0.0
Initial Release

152
README.md
View File

@@ -1,47 +1,153 @@
# @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}
},
// example 1: 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']
},
// 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']
},
}) })
``` ```
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 ✨
## 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
@@ -68,13 +174,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.

9892
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bitinflow/nuxt-oauth", "name": "@bitinflow/nuxt-oauth",
"version": "1.0.1", "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",

View File

@@ -1,18 +1,18 @@
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: { 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']
}, },
}) })

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,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 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',
logout: 'https://accounts.bitinflow.com/logout' userInfo: 'https://example.com/api/users/me',
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>({

View File

@@ -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()}`
}; };
@@ -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()}`
} }
const setBearer = async (token: string, tokenType: string, expires: number) => { return navigateTo(authConfig.redirect.logout, { external: true })
}
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,27 +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(() => {
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, { 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 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, { 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) => { 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, { external: true })
}
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
} }
if (user.value === undefined) { if (user.value === undefined) {
return navigateTo(authConfig.redirect.login) return navigateTo(authConfig.redirect.login, { external: true })
} }
}) })
@@ -29,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
View 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)
}

5598
yarn.lock Normal file

File diff suppressed because it is too large Load Diff