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,
|
ClientID: o.clientID,
|
||||||
ClientSecret: o.clientSecret,
|
ClientSecret: o.clientSecret,
|
||||||
}, o.oauthSkipTls)
|
}, 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)
|
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
|
package cmd
|
||||||
|
|
||||||
import (
|
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 = [{
|
const suiteKinds = [{
|
||||||
"name": "HTTP",
|
"name": "HTTP",
|
||||||
}, {
|
}, {
|
||||||
|
@ -311,7 +333,6 @@ API.GetVersion((d) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(dirtyVersion)
|
|
||||||
if (dirtyVersion && dirtyVersion.length > 0) {
|
if (dirtyVersion && dirtyVersion.length > 0) {
|
||||||
appVersionLink.value = appVersionLink.value + '/commit/' + d.message.replace(dirtyVersion[0], '')
|
appVersionLink.value = appVersionLink.value + '/commit/' + d.message.replace(dirtyVersion[0], '')
|
||||||
} else if (version && version.length > 0) {
|
} else if (version && version.length > 0) {
|
||||||
|
@ -491,11 +512,27 @@ API.GetVersion((d) => {
|
||||||
title="You need to login first."
|
title="You need to login first."
|
||||||
width="30%"
|
width="30%"
|
||||||
>
|
>
|
||||||
<a href="/token">
|
<el-collapse accordion="true">
|
||||||
<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">
|
<el-collapse-item title="Server in cloud" name="1">
|
||||||
<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>
|
<a href="/oauth2/token" target="_blank">
|
||||||
</svg>
|
<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">
|
||||||
</a>
|
<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>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,10 @@ export default defineConfig({
|
||||||
target: 'http://127.0.0.1:8080',
|
target: 'http://127.0.0.1:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/oauth': {
|
||||||
|
target: 'http://127.0.0.1:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -25,12 +25,12 @@ SOFTWARE.
|
||||||
package oauth
|
package oauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/linuxsuren/api-testing/pkg/util"
|
"github.com/linuxsuren/api-testing/pkg/util"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
@ -55,6 +55,7 @@ func NewAuth(provider OAuthProvider, config oauth2.Config, skipTlsVerify bool) *
|
||||||
config.Scopes = provider.MinimalScopes()
|
config.Scopes = provider.MinimalScopes()
|
||||||
config.Endpoint.TokenURL = fmt.Sprintf("%s%s", provider.GetServer(), provider.GetTokenURL())
|
config.Endpoint.TokenURL = fmt.Sprintf("%s%s", provider.GetServer(), provider.GetTokenURL())
|
||||||
config.Endpoint.AuthURL = fmt.Sprintf("%s%s", provider.GetServer(), provider.GetAuthURL())
|
config.Endpoint.AuthURL = fmt.Sprintf("%s%s", provider.GetServer(), provider.GetAuthURL())
|
||||||
|
config.Endpoint.DeviceAuthURL = "https://github.com/login/device/code"
|
||||||
return &auth{
|
return &auth{
|
||||||
provider: provider,
|
provider: provider,
|
||||||
config: config,
|
config: config,
|
||||||
|
@ -78,13 +79,14 @@ func (a *auth) Callback(w http.ResponseWriter, r *http.Request, pathParams map[s
|
||||||
}
|
}
|
||||||
log.Println("get code", code)
|
log.Println("get code", code)
|
||||||
|
|
||||||
tr := &http.Transport{
|
sslcli := util.TlsAwareHTTPClient(a.skipTlsVerify)
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: a.skipTlsVerify},
|
|
||||||
}
|
|
||||||
sslcli := &http.Client{Transport: tr}
|
|
||||||
ctx := context.WithValue(r.Context(), oauth2.HTTPClient, sslcli)
|
ctx := context.WithValue(r.Context(), oauth2.HTTPClient, sslcli)
|
||||||
|
|
||||||
token, err := a.config.Exchange(ctx, code, oauth2.VerifierOption(a.verifier))
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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)
|
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) {
|
func (a *auth) RequestCode(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
|
||||||
ref := r.Header.Get("Referer")
|
ref := r.Header.Get("Referer")
|
||||||
log.Println("callback host", r.Host)
|
log.Println("callback host", r.Host)
|
||||||
|
|
|
@ -25,13 +25,14 @@ SOFTWARE.
|
||||||
package oauth
|
package oauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/linuxsuren/api-testing/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OAuthProvider interface {
|
type OAuthProvider interface {
|
||||||
|
@ -81,12 +82,7 @@ func GetUserInfo(server OAuthProvider, token string, skipTlsVerify bool) (userIn
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
client := http.Client{
|
client := util.TlsAwareHTTPClient(skipTlsVerify)
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTlsVerify},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
if resp, err = client.Do(req); err != nil {
|
if resp, err = client.Do(req); err != nil {
|
||||||
return
|
return
|
||||||
|
|
|
@ -26,7 +26,6 @@ package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -131,12 +130,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
client := http.Client{
|
client := util.TlsAwareHTTPClient(true) // TODO should have a way to change it
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
contextDir := NewContextKeyBuilder().ParentDir().GetContextValueOrEmpty(ctx)
|
contextDir := NewContextKeyBuilder().ParentDir().GetContextValueOrEmpty(ctx)
|
||||||
if err = testcase.Request.Render(dataContext, contextDir); err != nil {
|
if err = testcase.Request.Render(dataContext, contextDir); err != nil {
|
||||||
return
|
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
|
// TODO only do this for unit testing, should remove it once we have a better way
|
||||||
if strings.HasPrefix(testcase.Request.API, "http://") {
|
if strings.HasPrefix(testcase.Request.API, "http://") {
|
||||||
client = *http.DefaultClient
|
client = http.DefaultClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// send the HTTP request
|
// send the HTTP request
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -10,6 +9,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/linuxsuren/api-testing/pkg/util"
|
||||||
unstructured "github.com/linuxsuren/unstructured/pkg"
|
unstructured "github.com/linuxsuren/unstructured/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,11 +64,7 @@ var client *http.Client
|
||||||
// GetClient returns a default client
|
// GetClient returns a default client
|
||||||
func GetClient() *http.Client {
|
func GetClient() *http.Client {
|
||||||
if client == nil {
|
if client == nil {
|
||||||
client = &http.Client{
|
client = util.TlsAwareHTTPClient(true) // TODO should have a way to change it
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,18 @@ package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"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 {
|
func GetDefaultCachedHTTPClient() *http.Client {
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Transport: defaultCachedClient,
|
Transport: defaultCachedClient,
|
||||||
|
@ -47,19 +54,13 @@ type cachedClient struct {
|
||||||
cacheData map[string][]byte
|
cacheData map[string][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type cachedResponse struct {
|
|
||||||
body []byte
|
|
||||||
header http.Header
|
|
||||||
status int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cachedClient) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (c *cachedClient) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
key := req.URL.String()
|
key := req.URL.String()
|
||||||
|
|
||||||
var cachedData *http.Response
|
var cachedData *http.Response
|
||||||
var ok bool
|
var ok bool
|
||||||
if cachedData, ok = c.cache[key]; ok {
|
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
|
return cachedData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +70,7 @@ func (c *cachedClient) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
}
|
}
|
||||||
var data []byte
|
var data []byte
|
||||||
if data, err = io.ReadAll(resp.Body); err == nil {
|
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.cache[key] = resp
|
||||||
c.cacheData[key] = data
|
c.cacheData[key] = data
|
||||||
|
|
Loading…
Reference in New Issue