From 21b66a103c25a5a1b457cbe9682d1d0a3ff94376 Mon Sep 17 00:00:00 2001 From: Laceyoo <51255903050@stu.ecnu.edu.cn> Date: Fri, 1 Dec 2023 15:03:50 +0800 Subject: [PATCH 1/2] support device flow mode --- controller/oauth2_device_flow.go | 72 ++ controller/route.go | 1 + front-standalone/package.json | 5 + front-standalone/src/api/playground.ts | 9 +- front-standalone/src/store/index.js | 15 +- .../playground/components/Authorization.vue | 5 +- .../views/playground/components/Client.vue | 2 +- .../views/playground/components/Device.vue | 793 ++++++++++++++++++ .../views/playground/components/Password.vue | 2 +- .../src/views/playground/index.vue | 17 +- 10 files changed, 908 insertions(+), 13 deletions(-) create mode 100644 controller/oauth2_device_flow.go create mode 100644 front-standalone/src/views/playground/components/Device.vue diff --git a/controller/oauth2_device_flow.go b/controller/oauth2_device_flow.go new file mode 100644 index 0000000..7909e4e --- /dev/null +++ b/controller/oauth2_device_flow.go @@ -0,0 +1,72 @@ +package controller + +import ( + "fmt" + "github.com/ECNU/Open-OAuth2Playground/g" + "github.com/ECNU/Open-OAuth2Playground/models" + "github.com/gin-gonic/gin" + "net/http" + "strconv" + "time" +) + +type ReqDeviceData struct { + ClientId string `json:"client_id"` + Code string `json:"code"` + ResponseType string `json:"response_type"` + ExpiresIn int `json:"expires_in"` +} + +func deviceFlow(c *gin.Context) { + reqData := ReqDeviceData{} + if err := c.Bind(&reqData); err != nil { + c.JSON(http.StatusOK, handleError(err.Error())) + return + } + + method := "POST" + apiAddr := g.Config().Endpoints.Token + + Code := reqData.Code + clientId := reqData.ClientId + ResponseType := reqData.ResponseType + ExpiresIn := reqData.ExpiresIn + body := fmt.Sprintf("code=%s&client_id=%s&response_type=%s", Code, clientId, ResponseType) + + header := make(map[string]string) + header["Content-Type"] = "application/x-www-form-urlencoded" + header["Content-Length"] = strconv.Itoa(len(body)) + + timeoutChan := time.After(time.Duration(ExpiresIn) * time.Second) // 设置expires_in秒后超时 + dataChan := make(chan models.Resp) + + // 启动一个协程循环检测token是否可用 + go func() { + for { + res, err := models.HandleRequest(method, apiAddr, g.UserAgent, body, g.Config().Timeout, header) + if err != nil { + c.JSON(http.StatusOK, handleError(err.Error())) + return + } + // 检查返回值中是否包含token + if rawJsonMap, ok := res.RawJson.(map[string]interface{}); ok { + if _, tokenAvailable := rawJsonMap["access_token"]; tokenAvailable { + dataChan <- res + return + } + } else { + fmt.Println("res.RawJson不是map格式") + } + time.Sleep(1000 * time.Millisecond) // 每隔1秒轮询 + } + }() + + // 已获取token或超时 + select { + case res := <-dataChan: + c.JSON(http.StatusOK, handleSuccess(res)) + //return res, nil + case <-timeoutChan: + c.JSON(http.StatusOK, handleError("user code expired")) + } +} diff --git a/controller/route.go b/controller/route.go index b3d1821..8186b47 100644 --- a/controller/route.go +++ b/controller/route.go @@ -19,6 +19,7 @@ func Routes(r *gin.Engine) { playground := r.Group(g.Config().Http.RouteBase + "v1") playground.Use(IPLimitCheck) playground.Use(NoCache()) + playground.POST("/oauth2/device_flow", deviceFlow) playground.POST("/oauth2/client_credentials", clientCredentials) playground.POST("/oauth2/password", passwordMode) playground.POST("/oauth2/authorization_code", exchangeTokenByCode) diff --git a/front-standalone/package.json b/front-standalone/package.json index 57ab0c3..f84e50e 100644 --- a/front-standalone/package.json +++ b/front-standalone/package.json @@ -10,27 +10,32 @@ "dependencies": { "@element-plus/icons-vue": "^1.1.4", "@highlightjs/vue-plugin": "^2.1.0", + "@types/crypto-js": "^4.2.1", "@typescript-eslint/eslint-plugin": "^5.59.8", "@typescript-eslint/parser": "^5.59.8", "@vue/cli-plugin-typescript": "^5.0.8", "@vue/eslint-config-typescript": "^11.0.3", "axios": "^0.27.2", "core-js": "^3.30.2", + "crypto-js": "^4.2.0", "eslint-plugin-vue": "^9.14.1", "highlight.js": "^11.8.0", "less": "^4.1.3", "less-loader": "^7.3.0", + "qrcode": "^1.5.3", "qs": "^6.11.2", "typescript": "^5.0.4", "vue": "^3.3.4", "vue-clipboard3": "^2.0.0", "vue-router": "^4.2.2", "vuex": "^4.1.0", + "vuex-persistedstate": "^4.1.0", "webpack": "^5.85.0" }, "devDependencies": { "@babel/eslint-parser": "^7.21.8", "@types/node": "^20.3.1", + "@types/qrcode": "^1.5.5", "@types/qs": "^6.9.7", "@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-eslint": "^5.0.8", diff --git a/front-standalone/src/api/playground.ts b/front-standalone/src/api/playground.ts index 3368c9f..7db46c0 100644 --- a/front-standalone/src/api/playground.ts +++ b/front-standalone/src/api/playground.ts @@ -38,4 +38,11 @@ export const fetchACTokenByClient = (data) => { /** Password */ export const fetchACTokenByPassword = (data) => { return http.post("/oauth2/password", data); -}; \ No newline at end of file +}; + +/** Device Flow */ +/** Step 2 */ +/** Get access_token with device_code */ +export const fetchACTokenByDevice = (data) => { + return http.post("/oauth2/device_flow", data); +}; diff --git a/front-standalone/src/store/index.js b/front-standalone/src/store/index.js index 9ddb3cf..324c607 100644 --- a/front-standalone/src/store/index.js +++ b/front-standalone/src/store/index.js @@ -1,19 +1,24 @@ import { createStore } from "vuex" +import createPersistedState from 'vuex-persistedstate'; const store = createStore({ state() { return { backendUrl: 'http://localhost:80', - authServerUrl: '' + authServerUrl: '', + grantTypes: "1" } }, mutations: { setBackEndUrl(state, v) { state.backendUrl = v - } - } + }, + setGrantTypes(state, v) { + state.grantTypes = v; + }, + }, + plugins: [createPersistedState()], }) - -export default store \ No newline at end of file +export default store diff --git a/front-standalone/src/views/playground/components/Authorization.vue b/front-standalone/src/views/playground/components/Authorization.vue index ee6cf6e..e513164 100644 --- a/front-standalone/src/views/playground/components/Authorization.vue +++ b/front-standalone/src/views/playground/components/Authorization.vue @@ -153,7 +153,7 @@ async function handleGetToken() { fetchACToken(dataObject).then(({code, msg, data}) => { if(code === 0){ const {request, response, rawjson, example} = data; - const {access_token, refresh_token} = rawjson || {};; + const {access_token, refresh_token} = rawjson || {}; currentToken.value = access_token??"Uncertain"; currentRefreshToken.value = refresh_token??"Uncertain"; s3CurrentToken.value = access_token??"Uncertain"; @@ -311,11 +311,10 @@ function formatJson(jsonStr) { // 格式化 JSON 内容 try { let resStr = JSON.stringify(JSON.parse(jsonStr), null, ' '); - // console.log(resStr); return resStr; } catch (error) { // 解析失败,返回原始内容 - console.log('格式化json失败'); + console.error('格式化json失败'); return jsonStr; } } diff --git a/front-standalone/src/views/playground/components/Client.vue b/front-standalone/src/views/playground/components/Client.vue index dce16f1..01b219c 100644 --- a/front-standalone/src/views/playground/components/Client.vue +++ b/front-standalone/src/views/playground/components/Client.vue @@ -217,7 +217,7 @@ function formatJson(jsonStr) { return resStr; } catch (error) { // 解析失败,返回原始内容 - console.log('格式化json失败'); + console.error('格式化json失败'); return jsonStr; } } diff --git a/front-standalone/src/views/playground/components/Device.vue b/front-standalone/src/views/playground/components/Device.vue new file mode 100644 index 0000000..1ff3d02 --- /dev/null +++ b/front-standalone/src/views/playground/components/Device.vue @@ -0,0 +1,793 @@ + + + + diff --git a/front-standalone/src/views/playground/components/Password.vue b/front-standalone/src/views/playground/components/Password.vue index 2596b34..78827b0 100644 --- a/front-standalone/src/views/playground/components/Password.vue +++ b/front-standalone/src/views/playground/components/Password.vue @@ -231,7 +231,7 @@ function formatJson(jsonStr) { return resStr; } catch (error) { // 解析失败,返回原始内容 - console.log('格式化json失败'); + console.error('格式化json失败'); return jsonStr; } } diff --git a/front-standalone/src/views/playground/index.vue b/front-standalone/src/views/playground/index.vue index 84866a1..d812f90 100644 --- a/front-standalone/src/views/playground/index.vue +++ b/front-standalone/src/views/playground/index.vue @@ -7,12 +7,14 @@ import { fetchConfig } from '/@/api/common' import { Setting } from '@element-plus/icons-vue' import { ElMessage } from 'element-plus' import { LocalStorageService } from "/@/utils/persistence" +import Device from "/@/views/playground/components/Device.vue"; +import store from "/@/store/index" defineOptions({ name: "Playground" }); -const grantTypes = ref("1"); +const grantTypes = ref(store.state.grantTypes) const configData = reactive({ trust_domain: [], authorization_endpoint: "", @@ -48,12 +50,18 @@ function handleSaveAccount() { ElMessage.success(configData.client_id); } +function changeGrantType(newV) { + grantTypes.value = newV; + store.commit('setGrantTypes', newV); +} + const computedPopWidth = computed(() => { return window.innerWidth < 640 ? "100%" : "60%"; }); onMounted(() => { getGlobalConfig(); + grantTypes.value = store.state.grantTypes }) @@ -66,10 +74,11 @@ onMounted(() => { OAuth 2.0 Playground - + Authorization Code Resource Owner Password Credentials Client Credentials + Device Flow @@ -77,6 +86,7 @@ onMounted(() => {