Skip to content

Commit

Permalink
WIP: enable oauth2 client
Browse files Browse the repository at this point in the history
  • Loading branch information
liut committed Jul 6, 2023
1 parent bc390ef commit fc6c503
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 8 deletions.
9 changes: 9 additions & 0 deletions service/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ SOCKS_PROXY_PASSWORD=
# HTTPS PROXY
HTTPS_PROXY=

# OAUTH2 client
CLIENT_ID=
CLIENT_SECRET=
OAUTH_PREFIX=https://my-oauth-example.online
OAUTH_AUTHZ_PATH=/authorize
OAUTH_TOKEN_PATH=/token
OAUTH_SCOPE=openid
OAUTH_FIELD_UID=uid
COOKIE_NAME=user
3 changes: 3 additions & 0 deletions service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@
"dependencies": {
"axios": "^1.3.4",
"chatgpt": "^5.1.2",
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"dotenv": "^16.0.3",
"esno": "^0.16.3",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"https-proxy-agent": "^5.0.1",
"isomorphic-fetch": "^3.0.0",
"node-fetch": "^3.3.0",
"simple-oauth2": "^5.0.0",
"socks-proxy-agent": "^7.0.0"
},
"devDependencies": {
Expand Down
78 changes: 78 additions & 0 deletions service/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import express from 'express'
import debug from 'debug'
import { AuthorizationCode } from 'simple-oauth2'
import type { User } from './types'

const log = debug('oauth')
const router = express.Router()

const config = {
client: {
id: process.env.CLIENT_ID,
secret: process.env.CLIENT_SECRET,
},
auth: {
tokenHost: process.env.OAUTH_PREFIX || 'https://my-oauth-example.online',
tokenPath: process.env.OAUTH_TOKEN_PATH || '/oauth/token',
authorizePath: process.env.OAUTH_AUTHZ_PATH || '/oauth/authorize',
},
}

const cookieName = process.env.COOKIE_NAME || 'user'
const scope = process.env.OAUTH_SCOPE || 'openid'
const fieldUid = process.env.OAUTH_FIELD_UID || 'uid'

const client = new AuthorizationCode(config)

const randomString = length =>
[...Array(length)].map(() => (~~(Math.random() * 36)).toString(36)).join('')

router.get('/start', (req, res) => {
const state = randomString(24)
const authorizationUri = client.authorizeURL({
redirect_uri: process.env.OAUTH_CALLBACK_URL || `${req.protocol}://${req.hostname}/auth/cb`,
scope,
state,
})
log('redirect to: %s', authorizationUri)
res.redirect(authorizationUri)
})

router.get('/cb', async (req, res) => {
const { code } = req.query
const options = { code }

try {
const resp = await client.getToken(options)
res.header('Refresh', '3;url=/')
log('The resulting token: %s', resp.token)
let outout = '<h3>Welcome back'
if (resp.token[fieldUid]) {
// TODO: more fields
const user: User = { uid: resp.token[fieldUid], hit: Date.now() }
res.cookie(cookieName, user, { httpOnly: true })
outout = `${outout} ${user.uid}`
}
outout = `${outout} <a href="/">click to home</a>`

res.send(outout)
}
catch (error) {
log('Access Token Error: %s', error.message)
}
})

router.get('/me', async (req, res) => {
const user: User = req.cookies[cookieName]
if (user)
res.json({ status: 'Success', data: user })

else
res.json({ status: 'Fail', message: 'need login', data: null })
})

router.get('/', (req, res) => {
res.send('<br><a href="/auth/start">Log in with OAuth</a>')
})

export { router }
14 changes: 10 additions & 4 deletions service/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import express from 'express'
import cookieParser from 'cookie-parser'
import type { RequestProps } from './types'
import type { ChatMessage } from './chatgpt'
import { chatConfig, chatReplyProcess, currentModel } from './chatgpt'
import { auth } from './middleware/auth'
import { auth, cookieName } from './middleware/auth'
import { limiter } from './middleware/limiter'
import { isNotEmptyString } from './utils/is'
import { router as authRouter } from './auth'

const app = express()
const router = express.Router()

app.use(express.static('public'))
app.use(express.json())
app.use(cookieParser())

app.all('*', (_, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
Expand Down Expand Up @@ -57,9 +60,10 @@ router.post('/config', auth, async (req, res) => {

router.post('/session', async (req, res) => {
try {
const user = req.cookies[cookieName]
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
const hasAuth = isNotEmptyString(AUTH_SECRET_KEY)
res.send({ status: 'Success', message: '', data: { auth: hasAuth, model: currentModel() } })
const hasAuth = isNotEmptyString(AUTH_SECRET_KEY) // TODO: option for oauth
res.send({ status: 'Success', message: '', data: { auth: hasAuth, model: currentModel(), user } })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
Expand All @@ -84,6 +88,8 @@ router.post('/verify', async (req, res) => {

app.use('', router)
app.use('/api', router)
app.use('/auth', authRouter)
app.set('trust proxy', 1)

app.listen(3002, () => globalThis.console.log('Server is running on port 3002'))
const port = process.env.SERVICE_PORT || 3002
app.listen(port, () => globalThis.console.log('Server is running on port', port))
16 changes: 13 additions & 3 deletions service/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { isNotEmptyString } from '../utils/is'

const cookieName = process.env.COOKIE_NAME || 'user'
const OAuthEnabled = isNotEmptyString(process.env.CLIENT_ID) && isNotEmptyString(process.env.CLIENT_SECRET)
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
const AuthKeyEnabled = isNotEmptyString(AUTH_SECRET_KEY)

const auth = async (req, res, next) => {
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
if (isNotEmptyString(AUTH_SECRET_KEY)) {
if (OAuthEnabled) {
const user = req.cookies[cookieName]
if (user)
next() // TODO: refresh the cookie lifetime
else res.json({ status: 'Unauthorized', message: 'Please authenticate.', data: null })
}
else if (AuthKeyEnabled) {
try {
const Authorization = req.header('Authorization')
if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim())
Expand All @@ -18,4 +28,4 @@ const auth = async (req, res, next) => {
}
}

export { auth }
export { auth, cookieName }
7 changes: 7 additions & 0 deletions service/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,10 @@ export interface ModelConfig {
}

export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined

export interface User {
uid?: string
name?: string
avatar?: string
hit?: number
}
8 changes: 8 additions & 0 deletions src/store/modules/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import { getToken, removeToken, setToken } from './helper'
import { store } from '@/store'
import { fetchSession } from '@/api'

export interface User {
uid?: string
name?: string
avatar?: string
hit?: number
}

interface SessionResponse {
auth: boolean
model: 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI'
user: User | null
}

export interface AuthState {
Expand Down
6 changes: 5 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ export default defineConfig((env) => {
port: 1002,
open: false,
proxy: {
'/auth': {
target: viteEnv.VITE_APP_API_BASE_URL,
changeOrigin: true, // 允许跨域
},
'/api': {
target: viteEnv.VITE_APP_API_BASE_URL,
changeOrigin: true, // 允许跨域
rewrite: path => path.replace('/api/', '/'),
// rewrite: path => path.replace('/api/', '/'),
},
},
},
Expand Down

0 comments on commit fc6c503

Please sign in to comment.