feat: add device oauth mode (#287)
* chore: reduce the http client usage having a central place to get the http client * feat: add device oauth mode --------- Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
This commit is contained in:
parent
d300c4b047
commit
8bf774a2c9
|
@ -258,7 +258,9 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
|
|||
ClientID: o.clientID,
|
||||
ClientSecret: o.clientSecret,
|
||||
}, o.oauthSkipTls)
|
||||
mux.HandlePath(http.MethodGet, "/token", authHandler.RequestCode)
|
||||
mux.HandlePath(http.MethodGet, "/oauth2/token", authHandler.RequestCode)
|
||||
mux.HandlePath(http.MethodGet, "/oauth2/getLocalCode", authHandler.RequestLocalCode)
|
||||
mux.HandlePath(http.MethodGet, "/oauth2/getUserInfoFromLocalCode", authHandler.RequestLocalToken)
|
||||
mux.HandlePath(http.MethodGet, "/oauth2/callback", authHandler.Callback)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,27 @@
|
|||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 API Testing Authors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
|
|
|
@ -292,6 +292,28 @@ watch(viewName, (val) => {
|
|||
});
|
||||
})
|
||||
|
||||
const deviceAuthActive = ref(0)
|
||||
const deviceAuthResponse = ref({
|
||||
user_code: '',
|
||||
verification_uri: '',
|
||||
device_code: ''
|
||||
})
|
||||
const deviceAuthNext = () => {
|
||||
if (deviceAuthActive.value++ > 2) {
|
||||
return
|
||||
}
|
||||
|
||||
if (deviceAuthActive.value === 1) {
|
||||
fetch('/oauth2/getLocalCode')
|
||||
.then(API.DefaultResponseProcess)
|
||||
.then((d) => {
|
||||
deviceAuthResponse.value = d
|
||||
})
|
||||
} else if (deviceAuthActive.value === 2) {
|
||||
window.location.href = '/oauth2/getUserInfoFromLocalCode?device_code=' + deviceAuthResponse.value.device_code
|
||||
}
|
||||
}
|
||||
|
||||
const suiteKinds = [{
|
||||
"name": "HTTP",
|
||||
}, {
|
||||
|
@ -311,7 +333,6 @@ API.GetVersion((d) => {
|
|||
return
|
||||
}
|
||||
|
||||
console.log(dirtyVersion)
|
||||
if (dirtyVersion && dirtyVersion.length > 0) {
|
||||
appVersionLink.value = appVersionLink.value + '/commit/' + d.message.replace(dirtyVersion[0], '')
|
||||
} else if (version && version.length > 0) {
|
||||
|
@ -491,11 +512,27 @@ API.GetVersion((d) => {
|
|||
title="You need to login first."
|
||||
width="30%"
|
||||
>
|
||||
<a href="/token">
|
||||
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" class="octicon octicon-mark-github v-align-middle color-fg-default">
|
||||
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<el-collapse accordion="true">
|
||||
<el-collapse-item title="Server in cloud" name="1">
|
||||
<a href="/oauth2/token" target="_blank">
|
||||
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" class="octicon octicon-mark-github v-align-middle color-fg-default">
|
||||
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="Server in local" name="2">
|
||||
<el-steps :active="deviceAuthActive" finish-status="success">
|
||||
<el-step title="Request Device Code" />
|
||||
<el-step title="Input Code"/>
|
||||
<el-step title="Finished" />
|
||||
</el-steps>
|
||||
|
||||
<div v-if="deviceAuthActive===1">
|
||||
Open <a :href="deviceAuthResponse.verification_uri" target="_blank">this link</a>, and type the code: <span>{{ deviceAuthResponse.user_code }}. Then click the next step button.</span>
|
||||
</div>
|
||||
<el-button style="margin-top: 12px" @click="deviceAuthNext">Next step</el-button>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -29,6 +29,10 @@ export default defineConfig({
|
|||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/oauth': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -25,12 +25,12 @@ SOFTWARE.
|
|||
package oauth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/util"
|
||||
"golang.org/x/oauth2"
|
||||
|
@ -55,6 +55,7 @@ func NewAuth(provider OAuthProvider, config oauth2.Config, skipTlsVerify bool) *
|
|||
config.Scopes = provider.MinimalScopes()
|
||||
config.Endpoint.TokenURL = fmt.Sprintf("%s%s", provider.GetServer(), provider.GetTokenURL())
|
||||
config.Endpoint.AuthURL = fmt.Sprintf("%s%s", provider.GetServer(), provider.GetAuthURL())
|
||||
config.Endpoint.DeviceAuthURL = "https://github.com/login/device/code"
|
||||
return &auth{
|
||||
provider: provider,
|
||||
config: config,
|
||||
|
@ -78,13 +79,14 @@ func (a *auth) Callback(w http.ResponseWriter, r *http.Request, pathParams map[s
|
|||
}
|
||||
log.Println("get code", code)
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: a.skipTlsVerify},
|
||||
}
|
||||
sslcli := &http.Client{Transport: tr}
|
||||
sslcli := util.TlsAwareHTTPClient(a.skipTlsVerify)
|
||||
ctx := context.WithValue(r.Context(), oauth2.HTTPClient, sslcli)
|
||||
|
||||
token, err := a.config.Exchange(ctx, code, oauth2.VerifierOption(a.verifier))
|
||||
a.getUserInfo(w, r, token, err)
|
||||
}
|
||||
|
||||
func (a *auth) getUserInfo(w http.ResponseWriter, r *http.Request, token *oauth2.Token, err error) {
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -102,6 +104,33 @@ func (a *auth) Callback(w http.ResponseWriter, r *http.Request, pathParams map[s
|
|||
http.Redirect(w, r, "/?access_token="+token.AccessToken, http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *auth) RequestLocalToken(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
|
||||
deviceCode := r.FormValue("device_code")
|
||||
response, ok := deviceAuthResponseMap[deviceCode]
|
||||
if !ok {
|
||||
http.Error(w, "device code not found", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := a.config.DeviceAccessToken(r.Context(), response)
|
||||
a.getUserInfo(w, r, token, err)
|
||||
}
|
||||
|
||||
var deviceAuthResponseMap = map[string]*oauth2.DeviceAuthResponse{}
|
||||
|
||||
func (a *auth) RequestLocalCode(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
|
||||
response, err := a.config.DeviceAuth(context.Background())
|
||||
if err != nil {
|
||||
log.Println("failed to get device auth", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
deviceAuthResponseMap[response.DeviceCode] = response
|
||||
|
||||
data, _ := json.Marshal(response)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (a *auth) RequestCode(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
|
||||
ref := r.Header.Get("Referer")
|
||||
log.Println("callback host", r.Host)
|
||||
|
|
|
@ -25,13 +25,14 @@ SOFTWARE.
|
|||
package oauth
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/util"
|
||||
)
|
||||
|
||||
type OAuthProvider interface {
|
||||
|
@ -81,12 +82,7 @@ func GetUserInfo(server OAuthProvider, token string, skipTlsVerify bool) (userIn
|
|||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTlsVerify},
|
||||
},
|
||||
}
|
||||
|
||||
client := util.TlsAwareHTTPClient(skipTlsVerify)
|
||||
var resp *http.Response
|
||||
if resp, err = client.Do(req); err != nil {
|
||||
return
|
||||
|
|
|
@ -26,7 +26,6 @@ package runner
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
@ -131,12 +130,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
|
|||
}
|
||||
}()
|
||||
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
client := util.TlsAwareHTTPClient(true) // TODO should have a way to change it
|
||||
contextDir := NewContextKeyBuilder().ParentDir().GetContextValueOrEmpty(ctx)
|
||||
if err = testcase.Request.Render(dataContext, contextDir); err != nil {
|
||||
return
|
||||
|
@ -165,7 +159,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
|
|||
|
||||
// TODO only do this for unit testing, should remove it once we have a better way
|
||||
if strings.HasPrefix(testcase.Request.API, "http://") {
|
||||
client = *http.DefaultClient
|
||||
client = http.DefaultClient
|
||||
}
|
||||
|
||||
// send the HTTP request
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -10,6 +9,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/util"
|
||||
unstructured "github.com/linuxsuren/unstructured/pkg"
|
||||
)
|
||||
|
||||
|
@ -64,11 +64,7 @@ var client *http.Client
|
|||
// GetClient returns a default client
|
||||
func GetClient() *http.Client {
|
||||
if client == nil {
|
||||
client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
client = util.TlsAwareHTTPClient(true) // TODO should have a way to change it
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
|
|
@ -26,11 +26,18 @@ package util
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func TlsAwareHTTPClient(insecure bool) *http.Client {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
|
||||
}
|
||||
return &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
func GetDefaultCachedHTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: defaultCachedClient,
|
||||
|
@ -47,19 +54,13 @@ type cachedClient struct {
|
|||
cacheData map[string][]byte
|
||||
}
|
||||
|
||||
type cachedResponse struct {
|
||||
body []byte
|
||||
header http.Header
|
||||
status int
|
||||
}
|
||||
|
||||
func (c *cachedClient) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
key := req.URL.String()
|
||||
|
||||
var cachedData *http.Response
|
||||
var ok bool
|
||||
if cachedData, ok = c.cache[key]; ok {
|
||||
cachedData.Body = ioutil.NopCloser(bytes.NewReader(c.cacheData[key]))
|
||||
cachedData.Body = io.NopCloser(bytes.NewReader(c.cacheData[key]))
|
||||
return cachedData, nil
|
||||
}
|
||||
|
||||
|
@ -69,7 +70,7 @@ func (c *cachedClient) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||
}
|
||||
var data []byte
|
||||
if data, err = io.ReadAll(resp.Body); err == nil {
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader(data))
|
||||
resp.Body = io.NopCloser(bytes.NewReader(data))
|
||||
}
|
||||
c.cache[key] = resp
|
||||
c.cacheData[key] = data
|
||||
|
|
Loading…
Reference in New Issue