mirror of
https://github.com/bitinflow/nuxt-oauth.git
synced 2026-03-13 13:45:59 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5571f4584e | ||
|
|
734b495ec0 | ||
|
|
1560ae2038 | ||
|
|
4b7d11f44d | ||
|
|
06feac925b | ||
|
|
f27a14c860 | ||
|
|
479e7d4b22 | ||
|
|
29915ebd3b | ||
|
|
81b48ac806 | ||
|
|
5454c9677b | ||
|
|
434c335e3f | ||
|
|
693f60a306 |
@@ -1,6 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## v2.0.1
|
||||
|
||||
## v1.0.6
|
||||
|
||||
## v1.0.5
|
||||
|
||||
## v1.0.4
|
||||
|
||||
## v1.0.3
|
||||
|
||||
58
README.md
58
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)
|
||||
|
||||
## Features
|
||||
|
||||
- 📦 OAuth2 authentication
|
||||
- 📦 Supports only one OAuth2 provider
|
||||
- 📦 Supports only implicit flow
|
||||
- 📦 Authorization Code Grant with PKCE (default)
|
||||
- 📦 Simple OAuth 2 Implicit Grant Token
|
||||
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
|
||||
|
||||
> **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
|
||||
@@ -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`.
|
||||
|
||||
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
|
||||
export default defineNuxtConfig({
|
||||
@@ -38,28 +50,40 @@ export default defineNuxtConfig({
|
||||
ssr: false,
|
||||
// or
|
||||
routeRules: {
|
||||
'/account/**': { ssr: false },
|
||||
'/auth/**': { ssr: false }
|
||||
'/dashboard/**': {ssr: false},
|
||||
'/whatever/**': {ssr: false}
|
||||
},
|
||||
|
||||
// using code response type (default)
|
||||
oauth: {
|
||||
redirect: {
|
||||
login: '/login',
|
||||
logout: '/',
|
||||
callback: '/login',
|
||||
home: '/home'
|
||||
},
|
||||
endpoints: {
|
||||
authorization: 'https://example.com/v1/oauth/authorization',
|
||||
userInfo: `https://example.com/api/users/me`,
|
||||
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']
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This will be your callback url (host is determined by `window.location.origin`):
|
||||
|
||||
- Callback: `http://localhost:3000/auth/login`
|
||||
|
||||
That's it! You can now use @bitinflow/nuxt-oauth in your Nuxt app ✨
|
||||
|
||||
## Development
|
||||
|
||||
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.
|
||||
|
||||
84
package-lock.json
generated
84
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@bitinflow/nuxt-oauth",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@bitinflow/nuxt-oauth",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "^3.2.2",
|
||||
@@ -17,6 +17,7 @@
|
||||
"@nuxt/module-builder": "^0.2.1",
|
||||
"@nuxt/schema": "^3.2.2",
|
||||
"@nuxt/test-utils": "^3.2.2",
|
||||
"axios": "^1.3.5",
|
||||
"changelogen": "^0.4.1",
|
||||
"eslint": "^8.34.0",
|
||||
"nuxt": "^3.2.2",
|
||||
@@ -2639,6 +2640,12 @@
|
||||
"integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==",
|
||||
"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": {
|
||||
"version": "10.4.13",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz",
|
||||
@@ -2672,6 +2679,17 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "7.2.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"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": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
@@ -5862,6 +5915,27 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
@@ -7337,6 +7411,12 @@
|
||||
"integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitinflow/nuxt-oauth",
|
||||
"version": "1.0.4",
|
||||
"version": "2.0.1",
|
||||
"description": "Nuxt 3 OAuth Module",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -35,6 +35,7 @@
|
||||
"@nuxt/module-builder": "^0.2.1",
|
||||
"@nuxt/schema": "^3.2.2",
|
||||
"@nuxt/test-utils": "^3.2.2",
|
||||
"axios": "^1.3.5",
|
||||
"changelogen": "^0.4.1",
|
||||
"eslint": "^8.34.0",
|
||||
"nuxt": "^3.2.2",
|
||||
|
||||
@@ -5,17 +5,9 @@ export default defineNuxtConfig({
|
||||
|
||||
oauth: {
|
||||
redirect: {
|
||||
login: '/login/', // sandbox appends / at the end of url
|
||||
logout: '/',
|
||||
callback: '/login/', // sandbox appends / at the end of url
|
||||
home: '/home'
|
||||
},
|
||||
endpoints: {
|
||||
authorization: 'https://api.sandbox.own3d.pro/v1/oauth/authorization',
|
||||
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']
|
||||
clientId: '98e1cb74-125a-4d60-b686-02c2f0c87521',
|
||||
scope: ['user:read']
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {useAuth} from "#imports";
|
||||
import {useAuth, useNuxtApp} from "#imports";
|
||||
|
||||
const {user, signOut} = await useAuth();
|
||||
|
||||
@@ -7,11 +7,20 @@ definePageMeta({
|
||||
middleware: ["auth"]
|
||||
})
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
$api.get('user')
|
||||
.then((response: any) => {
|
||||
console.log(response.data)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.log(error)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="user">
|
||||
Hello {{ user.name }}
|
||||
Hello {{ user.data.first_name }}
|
||||
|
||||
<button @click="signOut">
|
||||
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,25 @@ import defu from "defu";
|
||||
|
||||
// Module options TypeScript interface definition
|
||||
export interface ModuleOptions {
|
||||
redirect: {
|
||||
login: string,
|
||||
logout: string,
|
||||
callback: string,
|
||||
home: string
|
||||
redirect?: {
|
||||
login?: string,
|
||||
logout?: string,
|
||||
callback?: string,
|
||||
home?: string
|
||||
},
|
||||
endpoints: {
|
||||
authorization: string,
|
||||
userInfo: string,
|
||||
logout: string | null
|
||||
endpoints?: {
|
||||
authorization?: string,
|
||||
token?: string,
|
||||
userInfo?: string,
|
||||
logout?: string | null
|
||||
},
|
||||
clientId: string,
|
||||
scope: string[]
|
||||
refreshToken?: {
|
||||
maxAge: number,
|
||||
}
|
||||
clientId?: string,
|
||||
responseType?: 'token' | 'code',
|
||||
prompt?: '' | 'none' | 'login' | 'consent',
|
||||
scope?: string[]
|
||||
}
|
||||
|
||||
const defaults: ModuleOptions = {
|
||||
@@ -27,11 +33,17 @@ const defaults: ModuleOptions = {
|
||||
},
|
||||
endpoints: {
|
||||
authorization: 'https://accounts.bitinflow.com/oauth/authorize',
|
||||
userInfo: `https://accounts.bitinflow.com/api/v3/user`,
|
||||
token: 'https://accounts.bitinflow.com/oauth/token',
|
||||
userInfo: 'https://accounts.bitinflow.com/api/v3/user',
|
||||
logout: null,
|
||||
},
|
||||
refreshToken: {
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
},
|
||||
clientId: 'please-set-client-id',
|
||||
scope: ['user:read']
|
||||
responseType: 'code',
|
||||
prompt: '',
|
||||
scope: []
|
||||
}
|
||||
|
||||
export default defineNuxtModule<ModuleOptions>({
|
||||
@@ -40,7 +52,7 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
configKey: 'oauth'
|
||||
},
|
||||
defaults,
|
||||
setup (moduleOptions, nuxt) {
|
||||
setup(moduleOptions, nuxt) {
|
||||
const resolver = createResolver(import.meta.url)
|
||||
|
||||
const options = defu(moduleOptions, {
|
||||
@@ -48,7 +60,7 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
})
|
||||
|
||||
// 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, {
|
||||
...options
|
||||
})
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import {CookieRef, navigateTo, useCookie, useRuntimeConfig} from "#app";
|
||||
import {ModuleOptions} from "../../module";
|
||||
import {generateRandomString, getChallengeFromVerifier} from "../support";
|
||||
|
||||
declare interface ComposableOptions {
|
||||
fetchUserOnInitialization: boolean
|
||||
}
|
||||
|
||||
let user: CookieRef<any>;
|
||||
let accessToken: CookieRef<any>;
|
||||
let refreshToken: CookieRef<any>;
|
||||
|
||||
export default async (options: ComposableOptions = {
|
||||
fetchUserOnInitialization: false
|
||||
}) => {
|
||||
const user: CookieRef<any> = useCookie('oauth_user')
|
||||
const accessToken: CookieRef<any> = useCookie('oauth_access_token')
|
||||
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 {
|
||||
const response = await fetch(authConfig.endpoints.userInfo, {
|
||||
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
|
||||
const params = new URLSearchParams({
|
||||
client_id: authConfig.clientId,
|
||||
redirect_uri: window.location.origin + authConfig.redirect.callback,
|
||||
response_type: 'token',
|
||||
scope: authConfig.scope.join(' ')
|
||||
response_type: authConfig.responseType,
|
||||
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()}`
|
||||
};
|
||||
|
||||
@@ -58,11 +77,15 @@ export default async (options: ComposableOptions = {
|
||||
return navigateTo(authConfig.redirect.logout)
|
||||
}
|
||||
|
||||
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};
|
||||
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
|
||||
if (options.fetchUserOnInitialization) {
|
||||
await fetchUser()
|
||||
@@ -79,7 +102,10 @@ export default async (options: ComposableOptions = {
|
||||
signIn,
|
||||
signOut,
|
||||
setBearerToken,
|
||||
setRefreshToken,
|
||||
bearerToken,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
authConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,78 @@
|
||||
import {addRouteMiddleware, defineNuxtPlugin, navigateTo} from '#app'
|
||||
import {addRouteMiddleware, defineNuxtPlugin, navigateTo, useCookie} from '#app'
|
||||
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(() => {
|
||||
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) => {
|
||||
const {user, authConfig, setBearerToken} = await useAuth()
|
||||
const {user, authConfig, setBearerToken, setRefreshToken} = await useAuth()
|
||||
|
||||
if (to.path === authConfig.redirect.callback) {
|
||||
const queryParams = new URLSearchParams(to.query.toString());
|
||||
@@ -11,15 +80,12 @@ export default defineNuxtPlugin(() => {
|
||||
return navigateTo(authConfig.redirect.login)
|
||||
}
|
||||
|
||||
const hashParams = new URLSearchParams(to.hash.substring(1))
|
||||
if (authConfig.responseType === 'token') {
|
||||
return await resolveUsingToken(to, authConfig, setBearerToken)
|
||||
}
|
||||
|
||||
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)
|
||||
if (authConfig.responseType === 'code') {
|
||||
return await resolveUsingCode(to, authConfig, setBearerToken, setRefreshToken)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
36
src/runtime/support.ts
Normal file
36
src/runtime/support.ts
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user