diff --git a/README.md b/README.md index 7d91fb4..6dcc22c 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ export default defineNuxtConfig({ '/whatever/**': {ssr: false} }, - // using code response type (default) + // example 1: using code response type (default) oauth: { endpoints: { authorization: 'https://example.com/oauth/authorize', @@ -66,7 +66,7 @@ export default defineNuxtConfig({ scope: ['user:read'] }, - // using token response type (not recommended) + // example 2: using token response type (not recommended) oauth: { endpoints: { authorization: 'https://example.com/oauth/authorize', @@ -86,6 +86,69 @@ This will be your callback url (host is determined by `window.location.origin`): 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 ```bash diff --git a/package.json b/package.json index 5a38e7a..fe120c4 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@nuxt/module-builder": "^0.2.1", "@nuxt/schema": "^3.2.2", "@nuxt/test-utils": "^3.2.2", - "axios": "^1.3.5", + "axios": "^1.6.0", "changelogen": "^0.4.1", "eslint": "^8.34.0", "nuxt": "^3.2.2", diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index ca194ba..8a0e164 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -7,6 +7,11 @@ export default defineNuxtConfig({ redirect: { home: '/home' }, + cookies: { + options: { + domain: '*.own3d.pro' + } + }, clientId: '98e1cb74-125a-4d60-b686-02c2f0c87521', scope: ['user:read'] }, diff --git a/src/module.ts b/src/module.ts index 0f789c9..4f61692 100644 --- a/src/module.ts +++ b/src/module.ts @@ -18,6 +18,24 @@ export interface ModuleOptions { refreshToken?: { 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', @@ -32,9 +50,9 @@ const defaults: ModuleOptions = { home: '/' }, endpoints: { - authorization: 'https://accounts.bitinflow.com/oauth/authorize', - token: 'https://accounts.bitinflow.com/oauth/token', - userInfo: 'https://accounts.bitinflow.com/api/v3/user', + authorization: 'https://example.com/oauth/authorize', + token: 'https://example.com/oauth/token', + userInfo: 'https://example.com/api/users/me', logout: null, }, refreshToken: { diff --git a/src/runtime/composables/useAuth.ts b/src/runtime/composables/useAuth.ts index 5ef8285..86bf00c 100644 --- a/src/runtime/composables/useAuth.ts +++ b/src/runtime/composables/useAuth.ts @@ -1,6 +1,6 @@ -import {CookieRef, navigateTo, useCookie, useRuntimeConfig} from "#app"; +import {CookieRef, navigateTo, useRuntimeConfig} from "#app"; import {ModuleOptions} from "../../module"; -import {generateRandomString, getChallengeFromVerifier} from "../support"; +import { generateRandomString, getChallengeFromVerifier, useCookie } from '../support' declare interface ComposableOptions { fetchUserOnInitialization: boolean @@ -14,9 +14,9 @@ export default async (options: ComposableOptions = { fetchUserOnInitialization: false }) => { 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'); + 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 (): Promise => { try { @@ -36,7 +36,7 @@ export default async (options: ComposableOptions = { } const signIn = async (): Promise => { - const state = useCookie('oauth_state'); + const state = useCookie('oauth_state', authConfig); state.value = generateRandomString(); // create oauth authorization url @@ -50,7 +50,7 @@ export default async (options: ComposableOptions = { }) if (authConfig.responseType === 'code') { - const codeVerifier = useCookie('oauth_code_verifier'); + const codeVerifier = useCookie('oauth_code_verifier', authConfig); codeVerifier.value = generateRandomString(); params.set('code_challenge', await getChallengeFromVerifier(codeVerifier.value)) diff --git a/src/runtime/plugin.ts b/src/runtime/plugin.ts index 19324bf..1479856 100644 --- a/src/runtime/plugin.ts +++ b/src/runtime/plugin.ts @@ -1,7 +1,8 @@ -import {addRouteMiddleware, defineNuxtPlugin, navigateTo, useCookie} from '#app' +import {addRouteMiddleware, defineNuxtPlugin, navigateTo} from '#app' import useAuth from "./composables/useAuth" import {RouteLocationNormalized} from "vue-router"; import {ModuleOptions} from "../module"; +import {useCookie} from "./support"; interface AccessToken { access_token: string, @@ -39,8 +40,9 @@ export default defineNuxtPlugin(() => { if (to.query['code']) { const code = to.query['code'] as string; const stateFromRequest = to.query['state'] as string; - const stateFromCookie = useCookie('oauth_state'); - const codeVerifier = useCookie('oauth_code_verifier'); + console.log('options', authConfig.cookies?.options) + const stateFromCookie = useCookie('oauth_state', authConfig); + const codeVerifier = useCookie('oauth_code_verifier', authConfig); if (stateFromRequest !== stateFromCookie.value) { console.warn('State mismatch', stateFromRequest, stateFromCookie.value) diff --git a/src/runtime/support.ts b/src/runtime/support.ts index 7f777f4..5b71f8a 100644 --- a/src/runtime/support.ts +++ b/src/runtime/support.ts @@ -2,6 +2,9 @@ * 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) } @@ -34,3 +37,10 @@ function base64urlencode(a: any) { export async function getChallengeFromVerifier(v: any) { return base64urlencode(await sha256(v)); } + +export function useCookie(name: string, options: ModuleOptions): CookieRef { + const cookieName = options.cookies?.names?.[name] || name; + const cookiePrefix = options.cookies?.prefix || ''; + const cookieOptions = options.cookies?.options || {}; + return _useCookie(cookiePrefix + cookieName, cookieOptions) +} diff --git a/yarn.lock b/yarn.lock index b93366c..a86eebe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1680,12 +1680,12 @@ autoprefixer@^10.4.15: picocolors "^1.0.0" postcss-value-parser "^4.2.0" -axios@^1.3.5: - version "1.5.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267" - integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ== +axios@^1.6.0: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== dependencies: - follow-redirects "^1.15.0" + follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -2809,10 +2809,10 @@ flatted@^3.2.7: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.15.0: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== form-data@^4.0.0: version "4.0.0"