├── .github
└── workflows
│ └── jsonlint.yml
├── .gitignore
├── Dockerfile
├── README.md
├── browser
├── session.go
└── tasks.go
├── checks
├── assets
│ └── report.html
├── checks.go
├── checks_test.go
├── clickjacking.go
├── mapping.go
├── reporting.go
├── rules
│ ├── checks.json
│ └── test.json
└── steps.go
├── cmd
├── root.go
└── session_init.go
├── config-template.json
├── config
├── cli_options.go
├── oauth_config.go
└── templating.go
├── docs
└── KOAuth.png
├── go.mod
├── go.sum
├── main.go
└── oauth
├── errors.go
├── oauth_constants.go
├── oauth_flow.go
├── oauth_flow_test.go
└── url_utils.go
/.github/workflows/jsonlint.yml:
--------------------------------------------------------------------------------
1 | # runs
2 |
3 | name: CI
4 |
5 | on:
6 | push:
7 | branches: [ master ]
8 | pull_request:
9 | branches:
10 | - master
11 | workflow_dispatch:
12 |
13 | jobs:
14 | json-lint:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Install jq
19 | run: sudo apt-get update && sudo apt-get install -y jq
20 | - name: Checks for valid JSON in rules directory
21 | run: |
22 | for file in $(ls $GITHUB_WORKSPACE/checks/rules/)
23 | do
24 | echo $GITHUB_WORKSPACE/checks/rules/$file
25 | jq '.' $GITHUB_WORKSPACE/checks/rules/$file
26 | retVal=$?
27 | if [ $retVal -ne 0 ]; then
28 | echo $file FAILED
29 | echo Please ensure $file contains valid JSON
30 | exit $retVal
31 | fi
32 | done
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | config*.json
2 | session.json
3 | KOAuth
4 | KOAuth.exe
5 | testingconfigs/*
6 | output.json
7 | config/resources/checks2.json
8 | curltest
9 | output
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang
2 |
3 | WORKDIR /app
4 | COPY . .
5 | ENTRYPOINT ["/app/KOAuth"]
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KOAuth
2 | 
3 |
4 | OAuth 2.0 automated security scanner - Work in progress!
5 |
6 | OAuth 2.0 is difficult to create a re-usable dynamic scanner because the OAuth 2.0 specification allows redirects to be implemented using any method available to the user agent. This means you cannot simply look for a 3XX status code.
7 | For this reason, this scanner uses `chromedp` to run the checks in a browser environment.
8 |
9 | Additionally, OAuth authorization servers often diverge from the spec in small but
10 | frustrating ways. For example, requiring consent to be obtained each time a user
11 | attempts to perform an OAuth 2.0 flow, even if they have already consented and authenticated
12 | previously.
13 |
14 | This scanner attempts to provide configuration/CLI options to address these common scenarios.
15 |
16 | The scanner works by:
17 |
18 | 1. Running `chromedp` and having the user authenticate at an OAuth 2.0 Auth URL
19 | 2. Once a user's session is established, opens new tabs in the Chrome browser and runs
20 | various checks
21 |
22 | Usage:
23 | - Place OAuth 2.0 credentials/information in config JSON file. An example is in config-template.json
24 | - `go build`
25 | - `./KOAuth --config=configfile.json --checks=./config/resources/checks.json --timeout=4`
26 |
27 | By default, KOAuth will attempt to authenticate your browser session by performing a normal OAuth flow (which generally will prompt for authentication if you are not logged in),
28 | but you may provide an argument to the "--authentication-url" flag to authenticate at another URL. Once you have authenticated,
29 | you can press enter to signal that the scan is ready to be run in the browser.
30 |
31 | `./KOAuth --help` for explanation of cli flags
32 |
33 | The timeout option defines how long each tab will wait to be redirected to the redirect_uri
34 | before assuming the request failed.
35 |
36 |
37 | ## Checks
38 | Custom checks can be added by placing the checks into a JSON file and passing with the `--checks` flag.
39 | By default, the checks in `./config/resources/checks.json` will be used. An example check is shown
40 | below:
41 |
42 | ```
43 | {
44 | "name":"redirect-uri-add-subdomain",
45 | "risk":"medium",
46 | "description":"Adds a subdomain to redirect_uri",
47 | "requiresSupport":["implicit-flow-supported"],
48 | "references":"",
49 | "steps": [
50 | {
51 | "flowType":"implicit",
52 | "authURLParams":{"redirect_uri":["{{{REDIRECT_SCHEME}}}://maliciousdomain.{{{REDIRECT_DOMAIN}}}{{{REDIRECT_PATH}}}"]},
53 | "deleteURLParams":["redirect_uri"],
54 | "requiredOutcome": "FAIL"
55 | }
56 | ]
57 | }
58 | ```
59 |
60 | In the above check, the default `redirect_uri` from the provided config file is replaced with the same `redirecturi`,
61 | but with an additional subdomain added. This check also depends on the implicit flow being supported ("requiresSupport":["implicit-flow-supported"]). The "requiredOutcome" of this step is that it fails, meaning the OAuth flow fails,
62 | and thus the check passes (we were not redirected to the malicious domain).
63 |
64 | Another example check is shown below, which determines if PKCE is supported:
65 |
66 | ```
67 | {
68 | "name":"pkce-supported",
69 | "risk":"medium",
70 | "description":"Checks if PKCE is supported",
71 | "references":"",
72 | "steps": [
73 | {
74 | "flowType":"authorization-code",
75 | "references":"",
76 | "authURLParams":{
77 | "code_challenge":["rYfL4iLm9cMZnD3io44mnyitTKSECpgDzkPPecwrXtE"],
78 | "code_challenge_method":["S256"]
79 | },
80 | "tokenExchangeExtraParams":{
81 | "code_verifier":["randomjasdjiasiudaradsiasdmkue012939123891238912398123"]
82 | },
83 | "requiredOutcome": "SUCCEED"
84 | },
85 | {
86 | "flowType":"authorization-code",
87 | "references":"",
88 | "authURLParams":{
89 | "code_challenge":["q6IBwbTBNQdLVSKVzs06m7R8dJGXyUBtKHZSz3o3jW4="],
90 | "code_challenge_method":["S256"]
91 | },
92 | "tokenExchangeExtraParams":{
93 | "code_verifier":["bad-verifier"]
94 | },
95 | "requiredOutcome": "FAIL"
96 | }
97 | ]
98 | }
99 | ```
100 |
101 | Mustache templating can be used in these checks to take values from the OAuth config. The
102 | following fields are supported: REDIRECT_URI, REDIRECT_SCHEME, REDIRECT_DOMAIN, REDIRECT_PATH,
103 | CLIENT_ID, CLIENT_SECRET, SCOPES, AUTH_URL, TOKEN_URL. Example below shows using
104 | templating to add a redirect_uri parameter that adds a malicious subdomain to the _valid_
105 | redirect URI.
106 |
107 | ```"authURLParams":{"redirect_uri":["{{{REDIRECT_SCHEME}}}://maliciousdomain.{{{REDIRECT_DOMAIN}}}{{{REDIRECT_PATH}}}"]},```
108 |
109 | The "deleteURLParams" field is used to delete "required" oauth URL params in the
110 | authorization request. The "authURLParams" field adds the provided params to the
111 | authorization URL request, and these parameters will always be added _after_
112 | the params specified by "deleteURLParams" are deleted.
113 |
114 | In the previous example, the proper "redirect_uri" from the OAuth 2.0 config is replaced
115 | with the value of "https://maliciousdomain.h0.gs".
116 |
117 | For various checks, you may wish to provide malformed "redirect_uri"
118 | parameters or more than one. In this case where it's not obvious which
119 | "redirect_uri" should be waited to be redirected to, provide the
120 | "waitForRedirectTo" key to specify which URL the scanner should wait
121 | to be redirected to during the flow.
122 |
123 | ```"waitForRedirectTo":"https://malicious.h0.gs"```
124 |
125 | If the check JSON format does not work to automate a check, a custom check function can be added,
126 | mapping the name of a check to a custom function. An example of this is in ./checks/state.go,
127 | and the mapping is added in ./checks/mapping.go.
128 |
--------------------------------------------------------------------------------
/browser/session.go:
--------------------------------------------------------------------------------
1 | package browser
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/chromedp/chromedp"
8 | "github.com/morganc3/KOAuth/config"
9 | )
10 |
11 | // ChromeExecContext - Original parent chrome tab context
12 | var ChromeExecContext context.Context
13 |
14 | // ChromeExecContextCancel - Original parent chrome tab context cancel function
15 | var ChromeExecContextCancel context.CancelFunc
16 |
17 | // RunWithTimeOut - run chromedp actions with a specified timeout
18 | func RunWithTimeOut(ctx *context.Context, timeout time.Duration, actions []chromedp.Action) (context.Context, error) {
19 | timeoutContext, _ := context.WithTimeout(*ctx, timeout*time.Second)
20 | // defer timeoutCancel()
21 | return timeoutContext, chromedp.Run(timeoutContext, actions...)
22 | }
23 |
24 | // InitChromeSession - initialize chrome session, setting
25 | // options from CLI
26 | func InitChromeSession() context.CancelFunc {
27 | var chromeOpts []chromedp.ExecAllocatorOption
28 | headlessFlag := chromedp.Flag("headless", false)
29 | userAgentFlag := chromedp.UserAgent(config.GetOpt(config.FlagUserAgent))
30 | chromeOpts = append(chromedp.DefaultExecAllocatorOptions[:], headlessFlag, userAgentFlag)
31 |
32 | proxy := config.GetOpt(config.FlagProxy)
33 | if proxy != "" {
34 | // Be sure you trust your proxy server if you choose this option
35 | ignoreCerts := chromedp.Flag("ignore-certificate-errors", true)
36 | chromeOpts = append(chromeOpts,
37 | chromedp.ProxyServer(proxy),
38 | ignoreCerts,
39 | )
40 | }
41 | cx, cancel := chromedp.NewExecAllocator(context.Background(), chromeOpts...)
42 | ChromeExecContext = cx
43 | ChromeExecContextCancel = cancel
44 | return cancel
45 | }
46 |
--------------------------------------------------------------------------------
/browser/tasks.go:
--------------------------------------------------------------------------------
1 | package browser
2 |
3 | import (
4 | "context"
5 | "log"
6 | "net/url"
7 |
8 | "github.com/chromedp/cdproto/dom"
9 | "github.com/chromedp/cdproto/network"
10 | "github.com/chromedp/chromedp"
11 | )
12 |
13 | // WaitRedirect - Wait until we get a redirect to a URL that contains our redirect URI's host
14 | // There is no easy way to do this with the chromedp API's, so we
15 | // watch events until we get one that is a EventRequestWillBeSent type with
16 | // a URL of our redirectURI
17 | func WaitRedirect(ctx context.Context, host, path string) <-chan *url.URL {
18 | ch := make(chan *url.URL, 1)
19 | chromedp.ListenTarget(ctx, func(ev interface{}) {
20 | redirect, ok := ev.(*network.EventRequestWillBeSent)
21 | if ok {
22 | redirectURL, err := url.Parse(redirect.Request.URL)
23 | if len(redirect.Request.URLFragment) > 0 {
24 | redirectURL.Fragment = redirect.Request.URLFragment[1:] // remove '#'
25 | }
26 | if err != nil {
27 | log.Fatal("Got bad redirectURL from EventRequestWillBeSent object")
28 | }
29 |
30 | // if we are being redirected to the provided redirectURL
31 | if redirectURL.Host == host && redirectURL.Path == path {
32 | select {
33 | case <-ctx.Done():
34 | return
35 | case ch <- redirectURL:
36 | close(ch)
37 | return
38 | }
39 | }
40 | }
41 | })
42 | return ch
43 | }
44 |
45 | // ResponseHeaders - HTTP Response Headers from chromedp request
46 | type ResponseHeaders *map[string]interface{}
47 |
48 | // ResponseBody - HTTP response Body from chromedp request
49 | type ResponseBody *string
50 |
51 | // Load - TODO: FIX this, currently broken. attemps to get both body and http headers
52 | // note: body works for google.com and not example.com
53 | // and headers works for example.com and not google.com
54 | func Load(url string) (ResponseHeaders, ResponseBody) {
55 | chromeContext, cancelContext := chromedp.NewContext(context.Background())
56 | defer cancelContext()
57 |
58 | var response string
59 | var statusCode int64
60 | var responseHeaders map[string]interface{}
61 |
62 | runError := chromedp.Run(
63 | chromeContext,
64 | getFullResponse(
65 | chromeContext, url,
66 | map[string]interface{}{"User-Agent": "Mozilla/5.0"},
67 | &response, &statusCode, &responseHeaders))
68 |
69 | if runError != nil {
70 | // TODO: Currently giving errors around retrieving document body
71 | // log.Println(runError)
72 | }
73 | return &responseHeaders, &response
74 | }
75 |
76 | // gets full response including headers / status code
77 | func getFullResponse(chromeContext context.Context, url string, requestHeaders map[string]interface{}, response *string, statusCode *int64, responseHeaders *map[string]interface{}) chromedp.Tasks {
78 | chromedp.ListenTarget(chromeContext, func(event interface{}) {
79 | switch responseReceivedEvent := event.(type) {
80 | case *network.EventResponseReceived:
81 | response := responseReceivedEvent.Response
82 | if response.URL == url {
83 | *statusCode = response.Status
84 | *responseHeaders = response.Headers
85 | }
86 | }
87 | })
88 |
89 | return chromedp.Tasks{
90 | network.Enable(),
91 | network.SetExtraHTTPHeaders(network.Headers(requestHeaders)),
92 | chromedp.Navigate(url),
93 | chromedp.ActionFunc(func(ctx context.Context) error {
94 | node, err := dom.GetDocument().Do(ctx)
95 | if err != nil {
96 | return err
97 | }
98 | *response, err = dom.GetOuterHTML().WithNodeID(node.NodeID).Do(ctx)
99 |
100 | return err
101 | })}
102 | }
103 |
--------------------------------------------------------------------------------
/checks/assets/report.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | KOAuth Report
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
101 |
102 |
103 |
104 |
121 |
122 |
123 |
157 |
158 |
259 |
260 |
261 |
262 |
263 |
264 |
KOAuth Report
265 |
266 |
267 |
268 |
269 |
Findings
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
--------------------------------------------------------------------------------
/checks/checks.go:
--------------------------------------------------------------------------------
1 | package checks
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 |
9 | "github.com/chromedp/chromedp"
10 | "github.com/morganc3/KOAuth/config"
11 | "github.com/morganc3/KOAuth/oauth"
12 | )
13 |
14 | type state string
15 |
16 | const (
17 | pass state = "PASS" // Test passed
18 | fail state = "FAIL" // Test failed
19 | warn state = "WARN" // Warning, likely some issue with the test
20 | info state = "INFO" // Informational
21 | skip state = "SKIP" // Skipped for some reason
22 | )
23 |
24 | type checkType string
25 |
26 | const (
27 | support checkType = "support" // Check to see if something is supported
28 | normal checkType = "normal" // Normal check defined by provided JSON check file
29 | custom checkType = "custom" // Custom check that is mapped to a Go function
30 | )
31 |
32 | var checksList []*check // List of normal or custom checks
33 | var supportChecksList []*check // List of "support" checks
34 |
35 | type check struct {
36 | CheckName string `json:"name"`
37 | RiskRating string `json:"risk"`
38 | Description string `json:"description"`
39 | SkipReason string `json:"skipReason,omitempty"`
40 | References string `json:"references,omitempty"`
41 |
42 | // "Support" checks that must have succeeded
43 | // For example, checks involving PKCE won't run unless
44 | // the PKCE support check succeeds
45 | RequiresSupport []string `json:"requiresSupport,omitempty"`
46 |
47 | // Output message giving information about why the check failed
48 | failMessage string `json:"-"`
49 |
50 | // Output message giving information about an error that occurred during
51 | // the check
52 | errorMessage string `json:"-"`
53 |
54 | // Custom defined check function
55 | custom *customCheck `json:"-"`
56 |
57 | Steps []step `json:"steps"`
58 |
59 | // State contains result of the check
60 | state `json:"-"`
61 |
62 | CheckType checkType `json:"type,omitempty"`
63 | }
64 |
65 | type customCheckFunction func(*check, *context.Context) (state, error)
66 | type customCheckContext *context.Context
67 | type customCheck struct {
68 | checkFunction customCheckFunction
69 | checkContext customCheckContext
70 | }
71 |
72 | // Init - initializes checks by reading checks from files, identifying
73 | // custom definitions for checks, setting up support checks
74 | func Init(ctx context.Context, checkJSONFile string, promptFlag string) {
75 | mappings = getMappings()
76 | checksList = readChecks(ctx, checkJSONFile, promptFlag)
77 |
78 | // Remove checks of type "support" and add them to SupportChecksList
79 | // TODO: do this during reading checks so we don't have to remove later
80 | for i, c := range checksList {
81 | if c.CheckType == support {
82 | checksList = append(checksList[:i], checksList[i+1:]...) // remove
83 | supportChecksList = append(supportChecksList, c)
84 | }
85 | }
86 | }
87 |
88 | // identifies if a check is supported, if so, runs the check
89 | func (c *check) doCheck() {
90 | var state state
91 | var err error
92 | if !c.checkSupported() {
93 | c.SkipReason = "Check skipped due to missing support for checks defined in requiresSupport"
94 | c.state = skip
95 | return
96 | }
97 |
98 | if c.custom != nil {
99 | state, err = c.custom.checkFunction(c, c.custom.checkContext)
100 | } else {
101 | state = c.runCheck()
102 | }
103 | c.state = state
104 | if err != nil {
105 | c.errorMessage = err.Error()
106 | }
107 | }
108 |
109 | // DoChecks - completes each support check, followed by other checks
110 | func DoChecks() {
111 | for _, c := range supportChecksList { // Do support checks first to determine support
112 | c.doCheck()
113 | }
114 | for _, c := range checksList { // Do the rest of checks
115 | c.doCheck()
116 | }
117 | }
118 |
119 | // PrintResults - print basic Check results to console
120 | func PrintResults() {
121 | allChecks := append(supportChecksList, checksList...)
122 | for _, c := range allChecks {
123 | fmt.Println(c.CheckName, c.state)
124 | if c.state == warn {
125 | fmt.Println("\t", c.errorMessage)
126 | }
127 | fmt.Println("")
128 | }
129 | }
130 |
131 | // TODO checks:
132 | // scopes reflected at consent url
133 | // client secret not required
134 |
135 | // TODO: There should be error checking here for
136 | // different errors, such as if we get an "error" URL parameter
137 | // returned in redirect URI, or if there is an internal error
138 |
139 | // runCheck - runs each step of a check, returning the
140 | // state of the check, indicating its outcome
141 | func (c *check) runCheck() state {
142 | // TODO check if check should be skipped
143 | // documentation should be added to say if a check in some cases should be
144 | // skipped, we should add a skipMessage in checks.json and a skipfunction
145 | // to detect if it should be skipped
146 | for i, step := range c.Steps {
147 | state, _ := step.runStep()
148 | step.state = state
149 | c.Steps[i] = step
150 | if state == pass && step.RequiredOutcome == outcomeSucceed {
151 | continue
152 | }
153 | if state != pass && step.RequiredOutcome == outcomeFail {
154 | continue
155 | }
156 |
157 | // Check failed
158 | return fail
159 | }
160 | return pass
161 | }
162 |
163 | // Checks if required support checks passed
164 | func (c *check) checkSupported() bool {
165 | // if this is a support check, return true
166 | if c.CheckType == support {
167 | return true
168 | }
169 | requires := c.RequiresSupport
170 |
171 | // Checks if "supportCheck" passed for
172 | // whatever checks are required
173 | for _, r := range requires {
174 | if !supportExists(r) {
175 | return false
176 | }
177 | }
178 |
179 | // Anonymous function so that this isn't used anywhere else,
180 | // as it shouldn't be used unless all support checks have already been run
181 | getSupportedFlows := func() []string {
182 | supported := []string{}
183 | for _, check := range supportChecksList {
184 | switch check.CheckName {
185 | case "implicit-flow-supported":
186 | if check.state == pass {
187 | supported = append(supported, oauth.FlowImplicit)
188 | }
189 | case "authorization-code-flow-supported":
190 | if check.state == pass {
191 | supported = append(supported, oauth.FlowAuthorizationCode)
192 | }
193 | }
194 | }
195 | return supported
196 | }
197 | supportedFlows := getSupportedFlows()
198 |
199 | // no flow types are supported
200 | if len(supportedFlows) == 0 {
201 | return false
202 | }
203 |
204 | // Check if any flow type is available to support the steps of
205 | // this check
206 | for i, s := range c.Steps {
207 | switch s.FlowType {
208 | case oauth.FlowImplicit:
209 | if !sliceContains(supportedFlows, oauth.FlowImplicit) {
210 | return false
211 | }
212 | case oauth.FlowAuthorizationCode:
213 | if !sliceContains(supportedFlows, oauth.FlowAuthorizationCode) {
214 | return false
215 | }
216 | default:
217 | // This is the case where a flowtype for a step was not set,
218 | // so just update it to whatever flowtype is supported
219 | c.Steps[i].FlowType = supportedFlows[0]
220 | flowInstance := c.Steps[i].FlowInstance
221 | flowInstance.UpdateFlowType(supportedFlows[0])
222 |
223 | return true
224 | }
225 | }
226 |
227 | return true
228 | }
229 |
230 | // Checks if required support check passed
231 | func supportExists(name string) bool {
232 | for _, c := range supportChecksList {
233 | if name == c.CheckName && c.state == pass {
234 | return true
235 | }
236 | }
237 | return false
238 | }
239 |
240 | func readChecks(ctx context.Context, checkFile, promptFlag string) []*check {
241 | jsonBytes := config.GenerateChecksInput(checkFile)
242 | if len(jsonBytes) <= 0 {
243 | log.Fatalf("Error opening or parsing JSON file")
244 | }
245 |
246 | var ret []*check
247 | err := json.Unmarshal(jsonBytes, &ret)
248 | if err != nil {
249 | log.Fatalf("Error unmarshalling check JSON file:\n%s\n", err.Error())
250 | }
251 |
252 | ret, ctx = processChecks(ctx, ret, promptFlag)
253 |
254 | return ret
255 | }
256 |
257 | func processChecks(ctx context.Context, checks []*check, promptFlag string) ([]*check, context.Context) {
258 | var ret []*check
259 | currCtx := ctx
260 | for i, c := range checks {
261 | if c.CheckType == "" {
262 | c.CheckType = normal
263 | }
264 |
265 | switch c.CheckType {
266 | case custom:
267 | funcMapping := getMapping(c.CheckName)
268 | if funcMapping == nil {
269 | log.Fatal("No function mapping found for check of type CUSTOM")
270 | }
271 | newCtx, _ := chromedp.NewContext(currCtx)
272 | cust := customCheck{
273 | checkFunction: getMapping(c.CheckName),
274 | checkContext: &newCtx,
275 | }
276 | c.custom = &cust
277 | default: // normal or support checks
278 | for j, s := range c.Steps {
279 | var responseType oauth.FlowType
280 | switch s.FlowType {
281 | case oauth.FlowAuthorizationCode:
282 | responseType = oauth.AuthorizationCodeFlowResponseType
283 | case oauth.FlowImplicit:
284 | responseType = oauth.ImplicitFlowResponseType
285 | default:
286 | // if malformed or empty, leave this empty.
287 | // support will be determined later, and
288 | // this will be updated in the Step.runStep() method
289 | responseType = ""
290 | }
291 | // make a new context child for each tabs
292 | // update ctx to the current context of the new instance
293 | newCtx, newCancel := chromedp.NewContext(currCtx)
294 | checks[i].Steps[j].FlowInstance = oauth.NewInstance(newCtx, newCancel, responseType, promptFlag)
295 | currCtx = newCtx
296 | }
297 | }
298 |
299 | // append pointer to the check to our list
300 | ret = append(ret, checks[i])
301 | }
302 | return ret, currCtx
303 | }
304 |
--------------------------------------------------------------------------------
/checks/checks_test.go:
--------------------------------------------------------------------------------
1 | package checks
2 |
3 | // func TestAuthUrlFuncs(t *testing.T) {
4 | // authUrl := "https://google.com?client_id=28923189123&state=random_state&redirect_uri=http://example.com&response_type=code"
5 | // urlp, _ := url.Parse(authUrl)
6 | // deleteRequiredParams(urlp, []string{"redirect_uri", "client_id", "response_type"})
7 | // assert.Equal(t, "https://google.com?state=random_state", urlp.String())
8 |
9 | // urlp, _ = url.Parse(authUrl)
10 | // addAuthURLParams(urlp, map[string][]string{"redirect_uri": {"https://example.com"}})
11 | // assert.Equal(t, fmt.Sprintf("%s%s", authUrl, "&redirect_uri=https://example.com"), urlp.String())
12 |
13 | // urlp, _ = url.Parse(authUrl)
14 | // deleteRequiredParams(urlp, []string{"redirect_uri", "state"})
15 | // addAuthURLParams(urlp, map[string][]string{"redirect_uri": {"https://zzz1.com", "https://zzz2.com"}})
16 | // expected := "https://google.com?client_id=28923189123&response_type=code&redirect_uri=https://zzz1.com&redirect_uri=https://zzz2.com"
17 | // assert.Equal(t, expected, urlp.String())
18 | // }
19 |
--------------------------------------------------------------------------------
/checks/clickjacking.go:
--------------------------------------------------------------------------------
1 | package checks
2 |
3 | import (
4 | "context"
5 | "net/url"
6 | "strings"
7 | "time"
8 |
9 | "github.com/chromedp/cdproto/network"
10 | "github.com/chromedp/chromedp"
11 | "github.com/morganc3/KOAuth/browser"
12 | "github.com/morganc3/KOAuth/config"
13 | "github.com/morganc3/KOAuth/oauth"
14 | )
15 |
16 | // Custom check definition for clickjacking
17 | // this check cannot be easily implemented
18 | // via our checks JSON format
19 |
20 | func clickjackingCheck(c *check, ctx *context.Context) (state, error) {
21 | // listen network event
22 | authzCodeURL := oauth.GenerateAuthorizationURL(oauth.AuthorizationCodeFlowResponseType, "random-state", config.GetOpt(config.FlagPrompt))
23 |
24 | allHeaders := make(map[string][]string)
25 | domain := authzCodeURL.Host
26 | listenForNetworkEvent(*ctx, domain, allHeaders)
27 |
28 | actions := []chromedp.Action{network.Enable(),
29 | chromedp.Navigate(authzCodeURL.String()),
30 | chromedp.WaitVisible(`body`, chromedp.BySearch)}
31 |
32 | browser.RunWithTimeOut(ctx, time.Duration(config.GetOptAsInt(config.FlagTimeout)), actions)
33 |
34 | if allowsIframes(allHeaders) {
35 | return fail, nil
36 | }
37 |
38 | return pass, nil
39 | }
40 |
41 | // identify if headers are present that would prevent iframes
42 | func allowsIframes(allHeaders map[string][]string) bool {
43 | if vals, ok := allHeaders["x-frame-options"]; ok {
44 | for _, v := range vals {
45 | if v == "sameorigin" || v == "deny" {
46 | return false
47 | }
48 | }
49 | }
50 |
51 | if vals, ok := allHeaders["content-security-policy"]; ok {
52 | for _, v := range vals {
53 | if strings.Contains(v, "frame-ancestors") {
54 | return false
55 | }
56 | }
57 | }
58 |
59 | return true
60 | }
61 |
62 | func listenForNetworkEvent(ctx context.Context, domain string, allHeaders map[string][]string) {
63 | chromedp.ListenTarget(ctx, func(ev interface{}) {
64 | switch ev := ev.(type) {
65 |
66 | case *network.EventResponseReceived:
67 | resp := ev.Response
68 | respURL, _ := url.Parse(resp.URL)
69 | respDomain := respURL.Host
70 | // get headers set by our current domain that is being loaded
71 | // this isn't ideal, but it accounts for most edge cases
72 | // such as an iframe being responded to with a 302 (for example to a www. subdomain)
73 | // We can't simply check the headers of the first HTTP response because
74 | // of this reason
75 | domainContainsOther := (strings.Contains(respDomain, domain) || strings.Contains(domain, respDomain))
76 | if len(resp.Headers) != 0 && domainContainsOther {
77 | for k, v := range resp.Headers {
78 | key := strings.ToLower(k) // headers are not case sensitive
79 | val := strings.ToLower(v.(string))
80 | if _, ok := allHeaders[key]; ok {
81 | allHeaders[key] = append(allHeaders[key], val)
82 | } else {
83 | allHeaders[key] = []string{val}
84 | }
85 | }
86 | }
87 | return
88 | }
89 | })
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/checks/mapping.go:
--------------------------------------------------------------------------------
1 | package checks
2 |
3 | var mappings map[string]customCheckFunction
4 |
5 | // mappings of check names from JSON to functions
6 | // these are mappings of custom check functions
7 | // for checks that can't be accomplished with the
8 | // simple model defined in our checks.json structure/templating
9 | func getMappings() map[string]customCheckFunction {
10 | return map[string]customCheckFunction{
11 | "clickjacking-in-oauth-handshake": clickjackingCheck,
12 | }
13 | }
14 |
15 | func getMapping(name string) customCheckFunction {
16 | if v, ok := mappings[name]; ok {
17 | return v
18 | }
19 | return nil
20 | }
21 |
--------------------------------------------------------------------------------
/checks/reporting.go:
--------------------------------------------------------------------------------
1 | package checks
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "os"
9 | "path/filepath"
10 | "text/template"
11 |
12 | "github.com/morganc3/KOAuth/oauth"
13 | )
14 |
15 | // structs for output format
16 |
17 | type stepOut struct {
18 | //fields taken from Step.FlowInstance
19 | AuthorizationURL string `json:"authorizationURL"`
20 | RedirectedToURL string `json:"redirectedToURL"`
21 |
22 | FailMessage string `json:"failMessage,omitempty"`
23 | ErrorMessage string `json:"errorMessage,omitempty"`
24 |
25 | RequiredOutcome string `json:"requiredOutcome"`
26 |
27 | FlowType string `json:"flowType,omitempty"`
28 | FlowInstance *oauth.FlowInstance `json:"flow,omitempty"`
29 |
30 | // State contains result of the step
31 | State string `json:"state"`
32 | }
33 |
34 | type checkOut struct {
35 | CheckName string `json:"name"`
36 | RiskRating string `json:"risk"`
37 | Description string `json:"description"`
38 | SkipReason string `json:"skipReason,omitempty"`
39 | References string `json:"references,omitempty"`
40 | FailMessage string `json:"failMessage,omitempty"`
41 | ErrorMessage string `json:"errorMessage,omitempty"`
42 | Steps []stepOut `json:"steps,omitempty"`
43 | State string `json:"state"`
44 | }
45 |
46 | // convert Step to StepOut
47 | func (s *step) export() stepOut {
48 | return stepOut{
49 | AuthorizationURL: s.FlowInstance.AuthorizationURL.String(),
50 | RedirectedToURL: s.FlowInstance.RedirectedToURL.String(),
51 | FailMessage: s.failMessage,
52 | ErrorMessage: s.errorMessage,
53 | RequiredOutcome: s.RequiredOutcome,
54 | State: string(s.state),
55 | FlowType: s.FlowType,
56 | FlowInstance: s.FlowInstance,
57 | }
58 | }
59 |
60 | // WriteResults - Write Check results in JSON format to file
61 | // and generate HTML report
62 | func WriteResults(outDir string, htmlReportTemplate string) {
63 | outDir = removeTrailingSlash(outDir) // remove trailing slash from provided dir
64 | err := makeDirectory(outDir) // create output directory if it doesn't exist
65 |
66 | var outList []checkOut
67 | allChecks := append(supportChecksList, checksList...)
68 | for _, c := range allChecks {
69 | // only want to output some fields, so
70 | // marhsal Check struct to bytes, then unmarshal it back to tmp struct
71 | // then marshal to bytes and write to file
72 |
73 | var outCheck checkOut
74 | if c.state != skip {
75 | c.SkipReason = ""
76 | }
77 |
78 | bslice, err := json.Marshal(c)
79 | if err != nil {
80 | log.Fatalf("Could not Marshal to JSON for Check %s\n", c.CheckName)
81 | }
82 |
83 | err = json.Unmarshal(bslice, &outCheck)
84 | if err != nil {
85 | log.Fatalf("Could not Unmarshal to JSON to output format for %s\n", c.CheckName)
86 | }
87 |
88 | steps := c.Steps
89 | // Export steps to format for outputting
90 | outCheck.Steps = []stepOut{}
91 | for _, s := range steps {
92 | outCheck.Steps = append(outCheck.Steps, s.export())
93 | }
94 |
95 | outCheck.State = string(c.state)
96 | outList = append(outList, outCheck)
97 | }
98 |
99 | bslice, err := json.Marshal(outList)
100 | outFile := filepath.Join(outDir, "output.json")
101 |
102 | err = ioutil.WriteFile(outFile, bslice, 0644)
103 | if err != nil {
104 | log.Fatal(err)
105 | }
106 |
107 | htmlReportPath := filepath.Join(outDir, "report.html")
108 | renderTemplate(outList, htmlReportTemplate, htmlReportPath)
109 |
110 | fmt.Printf("HTML Report has been saved to %s\n", htmlReportPath)
111 | fmt.Printf("Raw JSON output has been saved to %s\n", outFile)
112 | }
113 |
114 | // remove trailing slash from output directory if present
115 | func removeTrailingSlash(outdir string) string {
116 | if string(outdir[len(outdir)-1]) == string(os.PathSeparator) { // remove trailing slash
117 | outdir = outdir[0 : len(outdir)-1]
118 | }
119 | return outdir
120 | }
121 |
122 | // create output directory if it doesn't exist
123 | func makeDirectory(outDir string) error {
124 | _, err := os.Stat(outDir)
125 | if err == nil { // directory exists
126 | return nil
127 | }
128 | if os.IsNotExist(err) { // directory does't exist, create it
129 | return os.MkdirAll(outDir, 0755)
130 | }
131 | return nil
132 | }
133 |
134 | // render html report template
135 | func renderTemplate(co []checkOut, htmlReportTemplate, htmlReportPath string) {
136 | t := template.New("HTML Report").Delims("[%[", "]%]")
137 |
138 | htmlFile, err := os.Open(htmlReportTemplate)
139 | if err != nil {
140 | log.Printf("Couldn't open file at %s\n", htmlReportTemplate)
141 | log.Fatal(err)
142 | }
143 |
144 | // read report html file
145 | tpl, err := ioutil.ReadAll(htmlFile)
146 | if err != nil {
147 | log.Fatal(err)
148 | }
149 |
150 | t, err = t.Parse(string(tpl))
151 | if err != nil {
152 | log.Println("Error parsing template")
153 | log.Fatal(err)
154 | }
155 |
156 | f, err := os.Create(htmlReportPath)
157 | if err != nil {
158 | log.Fatal(err)
159 | }
160 |
161 | bslice, err := json.Marshal(co)
162 | t.Execute(f, string(bslice))
163 | }
164 |
--------------------------------------------------------------------------------
/checks/rules/checks.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "pkce-supported",
4 | "risk": "medium",
5 | "type": "support",
6 | "description": "Checks if PKCE is supported",
7 | "references": "",
8 | "steps": [
9 | {
10 | "flowType": "authorization-code",
11 | "references": "",
12 | "authURLParams": {
13 | "code_challenge": [
14 | "rYfL4iLm9cMZnD3io44mnyitTKSECpgDzkPPecwrXtE"
15 | ],
16 | "code_challenge_method": [
17 | "S256"
18 | ]
19 | },
20 | "tokenExchangeExtraParams": {
21 | "code_verifier": [
22 | "randomjasdjiasiudaradsiasdmkue012939123891238912398123"
23 | ]
24 | },
25 | "requiredOutcome": "SUCCEED"
26 | },
27 | {
28 | "flowType": "authorization-code",
29 | "references": "",
30 | "authURLParams": {
31 | "code_challenge": [
32 | "q6IBwbTBNQdLVSKVzs06m7R8dJGXyUBtKHZSz3o3jW4="
33 | ],
34 | "code_challenge_method": [
35 | "S256"
36 | ]
37 | },
38 | "tokenExchangeExtraParams": {
39 | "code_verifier": [
40 | "bad-verifier"
41 | ]
42 | },
43 | "requiredOutcome": "FAIL"
44 | }
45 | ]
46 | },
47 | {
48 | "name": "state-supported-implicit",
49 | "risk": "medium",
50 | "type": "support",
51 | "description": "Checks if state parameter is supported for the implicit flow",
52 | "references": "",
53 | "steps": [
54 | {
55 | "flowType": "implicit",
56 | "references": "",
57 | "authURLParams": {
58 | "state": [
59 | "MyRandomStateValue"
60 | ]
61 | },
62 | "deleteURLParams": [
63 | "state"
64 | ],
65 | "redirectMustContainFragment": {
66 | "state": [
67 | "MyRandomStateValue"
68 | ]
69 | },
70 | "requiredOutcome": "SUCCEED"
71 | }
72 | ]
73 | },
74 | {
75 | "name": "state-supported-authorization-code",
76 | "risk": "medium",
77 | "type": "support",
78 | "description": "Checks if state parameter is supported for the authorization code flow",
79 | "references": "",
80 | "steps": [
81 | {
82 | "flowType": "authorization-code",
83 | "references": "",
84 | "authURLParams": {
85 | "state": [
86 | "MyRandomStateValue"
87 | ]
88 | },
89 | "deleteURLParams": [
90 | "state"
91 | ],
92 | "redirectMustContainUrl": {
93 | "state": [
94 | "MyRandomStateValue"
95 | ]
96 | },
97 | "requiredOutcome": "SUCCEED"
98 | }
99 | ]
100 | },
101 | {
102 | "name": "implicit-flow-supported",
103 | "risk": "info",
104 | "type": "support",
105 | "description": "Checks if the implicit flow is supported",
106 | "references": "",
107 | "steps": [
108 | {
109 | "flowType": "implicit",
110 | "references": "",
111 | "requiredOutcome": "SUCCEED"
112 | }
113 | ]
114 | },
115 | {
116 | "name": "authorization-code-flow-supported",
117 | "risk": "info",
118 | "type": "support",
119 | "description": "Checks if the authorization code flow is supported",
120 | "references": "",
121 | "steps": [
122 | {
123 | "flowType": "authorization-code",
124 | "references": "",
125 | "requiredOutcome": "SUCCEED"
126 | }
127 | ]
128 | },
129 | {
130 | "name": "redirect-uri-total-change",
131 | "risk": "high",
132 | "description": "Completely alters the redirect URI",
133 | "references": "",
134 | "steps": [
135 | {
136 | "authURLParams": {
137 | "redirect_uri": [
138 | "https://maliciousdomain.h0.gs"
139 | ]
140 | },
141 | "deleteURLParams": [
142 | "redirect_uri"
143 | ],
144 | "requiredOutcome": "FAIL"
145 | }
146 | ]
147 | },
148 | {
149 | "name": "redirect-uri-add-higher-domain",
150 | "risk": "medium",
151 | "description": "Adds a higher level domain to redirect_uri",
152 | "references": "",
153 | "steps": [
154 | {
155 | "authURLParams": {
156 | "redirect_uri": [
157 | "{{{REDIRECT_SCHEME}}}://{{{REDIRECT_DOMAIN}}}.maliciousdomain.com{{{REDIRECT_PATH}}}"
158 | ]
159 | },
160 | "deleteURLParams": [
161 | "redirect_uri"
162 | ],
163 | "requiredOutcome": "FAIL"
164 | }
165 | ]
166 | },
167 | {
168 | "name": "redirect-uri-add-subdomain",
169 | "risk": "medium",
170 | "description": "Adds a subdomain to redirect_uri",
171 | "references": "",
172 | "steps": [
173 | {
174 | "authURLParams": {
175 | "redirect_uri": [
176 | "{{{REDIRECT_SCHEME}}}://maliciousdomain.{{{REDIRECT_DOMAIN}}}{{{REDIRECT_PATH}}}"
177 | ]
178 | },
179 | "deleteURLParams": [
180 | "redirect_uri"
181 | ],
182 | "requiredOutcome": "FAIL"
183 | }
184 | ]
185 | },
186 | {
187 | "name": "redirect-uri-scheme-downgrade",
188 | "risk": "high",
189 | "description": "Downgrades scheme of redirect URI from HTTPS to HTTP",
190 | "skipReason": "This check was skipped because the proper redirect URI did not use the HTTPS scheme",
191 | "references": "",
192 | "steps": [
193 | {
194 | "authURLParams": {
195 | "redirect_uri": [
196 | "http://{{{REDIRECT_DOMAIN}}}{{{REDIRECT_PATH}}}"
197 | ]
198 | },
199 | "deleteURLParams": [
200 | "redirect_uri"
201 | ],
202 | "requiredOutcome": "FAIL"
203 | }
204 | ]
205 | },
206 | {
207 | "name": "redirect-uri-total-path-change",
208 | "risk": "high",
209 | "description": "Changes the path of the redirect URI",
210 | "references": "",
211 | "steps": [
212 | {
213 | "authURLParams": {
214 | "redirect_uri": [
215 | "{{{REDIRECT_SCHEME}}}://{{{REDIRECT_DOMAIN}}}/maliciouspath"
216 | ]
217 | },
218 | "deleteURLParams": [
219 | "redirect_uri"
220 | ],
221 | "requiredOutcome": "FAIL"
222 | }
223 | ]
224 | },
225 | {
226 | "name": "redirect-uri-path-append",
227 | "risk": "medium",
228 | "description": "Appends to the redirect_uri path",
229 | "references": "",
230 | "steps": [
231 | {
232 | "authURLParams": {
233 | "redirect_uri": [
234 | "{{{REDIRECT_SCHEME}}}://{{{REDIRECT_DOMAIN}}}{{{REDIRECT_PATH}}}/maliciousaddition"
235 | ]
236 | },
237 | "deleteURLParams": [
238 | "redirect_uri"
239 | ],
240 | "requiredOutcome": "FAIL"
241 | }
242 | ]
243 | },
244 | {
245 | "name": "redirect-uri-two-provided-redirect-uris",
246 | "risk": "medium",
247 | "description": "Two redirect uri's were provided, one is correct and one is incorrect. Ensure we are not redirected to the incorrect URI.",
248 | "references": "",
249 | "steps": [
250 | {
251 | "authURLParams": {
252 | "redirect_uri": [
253 | "{{{REDIRECT_SCHEME}}}://malicioussdomain.h0.gs{{{REDIRECT_PATH}}}"
254 | ]
255 | },
256 | "deleteURLParams": [
257 | "redirect_uri"
258 | ],
259 | "waitForRedirectTo": "{{{REDIRECT_SCHEME}}}://malicioussdomain.h0.gs{{{REDIRECT_PATH}}}",
260 | "requiredOutcome": "FAIL"
261 | }
262 | ]
263 | },
264 | {
265 | "name": "redirect-uri-improper-parsing",
266 | "risk": "high",
267 | "description": "Attempt to trick redirect URI parse using \"@\"",
268 | "references": "",
269 | "steps": [
270 | {
271 | "authURLParams": {
272 | "redirect_uri": [
273 | "{{{REDIRECT_SCHEME}}}://{{{REDIRECT_DOMAIN}}}@malicious.h0.gs"
274 | ]
275 | },
276 | "deleteURLParams": [
277 | "redirect_uri"
278 | ],
279 | "waitForRedirectTo": "https://malicious.h0.gs",
280 | "requiredOutcome": "FAIL"
281 | }
282 | ]
283 | },
284 | {
285 | "name": "redirect-uri-changed-to-localhost",
286 | "risk": "low",
287 | "description": "Checks if the server allows redirects to localhost, which is often enabled for debugging purposes",
288 | "references": "",
289 | "steps": [
290 | {
291 | "authURLParams": {
292 | "redirect_uri": [
293 | "http://localhost"
294 | ]
295 | },
296 | "deleteURLParams": [
297 | "redirect_uri"
298 | ],
299 | "requiredOutcome": "FAIL"
300 | }
301 | ]
302 | },
303 | {
304 | "name": "redirect-uri-contains-localhost",
305 | "risk": "high",
306 | "description": "Checks if the server allows redirects to a domain containing localhost",
307 | "references": "",
308 | "steps": [
309 | {
310 | "authURLParams": {
311 | "redirect_uri": [
312 | "http://localhost.malicious.com"
313 | ]
314 | },
315 | "deleteURLParams": [
316 | "redirect_uri"
317 | ],
318 | "requiredOutcome": "FAIL"
319 | }
320 | ]
321 | },
322 | {
323 | "name": "pkce-short-challenge",
324 | "risk": "low",
325 | "description": "Attempts to perform a PKCE flow with a short, guessable code verifier. Code verifier should have a minimum length of 43 characters.",
326 | "requiresSupport": [
327 | "pkce-supported"
328 | ],
329 | "references": "",
330 | "steps": [
331 | {
332 | "flowType": "authorization-code",
333 | "references": "",
334 | "authURLParams": {
335 | "code_challenge": [
336 | "Nb9gqlOcQmdgooA-8xjf8IPMQhWeyujCph4yzdaXdH0"
337 | ],
338 | "code_challenge_method": [
339 | "S256"
340 | ]
341 | },
342 | "tokenExchangeExtraParams": {
343 | "code_verifier": [
344 | "short-verifier"
345 | ]
346 | },
347 | "requiredOutcome": "FAIL"
348 | }
349 | ]
350 | },
351 | {
352 | "name": "pkce-downgrade",
353 | "risk": "medium",
354 | "description": "Attempts to downgrade from PKCE, by never sending the code_verifier in the exchange request",
355 | "requiresSupport": [
356 | "pkce-supported"
357 | ],
358 | "references": "",
359 | "steps": [
360 | {
361 | "flowType": "authorization-code",
362 | "references": "",
363 | "authURLParams": {
364 | "code_challenge": [
365 | "rYfL4iLm9cMZnD3io44mnyitTKSECpgDzkPPecwrXtE"
366 | ],
367 | "code_challenge_method": [
368 | "S256"
369 | ]
370 | },
371 | "requiredOutcome": "FAIL"
372 | }
373 | ]
374 | },
375 | {
376 | "name": "pkce-downgrade-to-plain",
377 | "risk": "medium",
378 | "description": "Attempts to send same value for code_challenge and code_verifier (downgrade from S256 to plain)",
379 | "requiresSupport": [
380 | "pkce-supported"
381 | ],
382 | "references": "",
383 | "steps": [
384 | {
385 | "flowType": "authorization-code",
386 | "references": "",
387 | "authURLParams": {
388 | "code_challenge": [
389 | "rYfL4iLm9cMZnD3io44mnyitTKSECpgDzkPPecwrXtE"
390 | ],
391 | "code_challenge_method": [
392 | "S256"
393 | ]
394 | },
395 | "tokenExchangeExtraParams": {
396 | "code_verifier": [
397 | "rYfL4iLm9cMZnD3io44mnyitTKSECpgDzkPPecwrXtE"
398 | ]
399 | },
400 | "requiredOutcome": "FAIL"
401 | }
402 | ]
403 | },
404 | {
405 | "name": "pkce-plain-supported1",
406 | "risk": "medium",
407 | "description": "Checks if code_challenge_method of \"plain\" is supported",
408 | "requiresSupport": [
409 | "pkce-supported"
410 | ],
411 | "references": "",
412 | "steps": [
413 | {
414 | "flowType": "authorization-code",
415 | "references": "",
416 | "authURLParams": {
417 | "code_challenge": [
418 | "randomjasdjiasiudaradsiasdmkue012939123891238912398123"
419 | ],
420 | "code_challenge_method": [
421 | "plain"
422 | ]
423 | },
424 | "tokenExchangeExtraParams": {
425 | "code_verifier": [
426 | "randomjasdjiasiudaradsiasdmkue012939123891238912398123"
427 | ]
428 | },
429 | "requiredOutcome": "FAIL"
430 | }
431 | ]
432 | },
433 | {
434 | "name": "pkce-plain-supported2",
435 | "risk": "medium",
436 | "description": "Checks if code_challenge_method with \"plain\" is supported",
437 | "requiresSupport": [
438 | "pkce-supported"
439 | ],
440 | "references": "",
441 | "steps": [
442 | {
443 | "flowType": "authorization-code",
444 | "references": "",
445 | "authURLParams": {
446 | "code_challenge": [
447 | "randomjasdjiasiudaradsiasdmkue012939123891238912398123"
448 | ]
449 | },
450 | "tokenExchangeExtraParams": {
451 | "code_verifier": [
452 | "randomjasdjiasiudaradsiasdmkue012939123891238912398123"
453 | ]
454 | },
455 | "requiredOutcome": "FAIL"
456 | }
457 | ]
458 | },
459 | {
460 | "name": "clickjacking-in-oauth-handshake",
461 | "type": "custom",
462 | "risk": "high",
463 | "description": "iframes are not prevented in the consent screen. This is particularly dangeorus for the OAuth handshake, as generally granting consent involves one single click. This can, in many cases, lead to a clickjacking attack that allows a single-click clickjacking account takeover attack.",
464 | "references":""
465 | }
466 | ]
467 |
--------------------------------------------------------------------------------
/checks/rules/test.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "implicit-flow-supported",
4 | "risk": "info",
5 | "description": "Checks if the implicit flow is supported",
6 | "type": "support",
7 | "references": "",
8 | "steps": [
9 | {
10 | "flowType": "implicit",
11 | "references": "",
12 | "requiredOutcome": "SUCCEED"
13 | }
14 | ]
15 | },
16 | {
17 | "name": "authorization-code-flow-supported",
18 | "risk": "info",
19 | "description": "Checks if the authorization code flow is supported",
20 | "type": "support",
21 | "references": "",
22 | "steps": [
23 | {
24 | "flowType": "authorization-code",
25 | "references": "",
26 | "requiredOutcome": "SUCCEED"
27 | }
28 | ]
29 | },
30 | {
31 | "name": "redirect-uri-total-change",
32 | "risk": "high",
33 | "description": "Completely alters the redirect URI",
34 | "references": "",
35 | "steps": [
36 | {
37 | "authURLParams": {
38 | "redirect_uri": [
39 | "https://maliciousdomain.h0.gs"
40 | ]
41 | },
42 | "deleteURLParams": [
43 | "redirect_uri"
44 | ],
45 | "requiredOutcome": "FAIL"
46 | }
47 | ]
48 | }
49 | ]
50 |
--------------------------------------------------------------------------------
/checks/steps.go:
--------------------------------------------------------------------------------
1 | package checks
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "net/url"
9 |
10 | "github.com/morganc3/KOAuth/oauth"
11 | )
12 |
13 | const (
14 | outcomeFail = "FAIL"
15 | outcomeSucceed = "SUCCEED"
16 | )
17 |
18 | type step struct {
19 | // Flow type, authorization-code and implicit are supported
20 | // if this is empty, it will default to whichever flow is supported
21 | // flow, prioritizing implicit
22 | FlowType string `json:"flowType,omitempty"`
23 |
24 | // Extra parameters to be added to Auth URL
25 | AuthURLParams map[string][]string `json:"authUrlParams,omitempty"`
26 |
27 | // Default parameters that should be deleted prior to browsing to Auth URL
28 | DeleteAuthURLParams []string `json:"deleteUrlParams,omitempty"`
29 |
30 | // Not taken as input, default params required for the code exchange
31 | TokenExchangeParams url.Values `json:"-"`
32 |
33 | // Extra parameters to be added to the code exchange
34 | TokenExchangeExtraParams map[string][]string `json:"tokenExchangeExtraParams,omitempty"`
35 |
36 | // Default parameters that should be deleted prior to code exchange
37 | DeleteTokenExchangeParams []string `json:"deleteExchangeParams,omitempty"`
38 |
39 | // URL to wait to be redirected to
40 | WaitForRedirectTo string `json:"waitForRedirectTo,omitempty"`
41 |
42 | // URL Parameters that must be in URL we are redirected to
43 | RedirectMustContainURL map[string][]string `json:"redirectMustContainUrl,omitempty"`
44 |
45 | // Fragment Parameters that must be in URL we are redirected to
46 | RedirectMustContainFragment map[string][]string `json:"redirectMustContainFragment,omitempty"`
47 |
48 | failMessage string `json:"-"`
49 | errorMessage string `json:"-"`
50 |
51 | RequiredOutcome string `json:"requiredOutcome"`
52 |
53 | // State contains result of the step
54 | state `json:"state"`
55 |
56 | FlowInstance *oauth.FlowInstance `json:"flow,omitempty"`
57 | }
58 |
59 | func (s *step) runStep() (state, error) {
60 | fi := s.FlowInstance
61 | authzURL := fi.AuthorizationURL
62 |
63 | // first delete any "required" auth URL parameters that we have specfically
64 | // defined in the check to be deleted
65 | deleteRequiredParams(authzURL, s.DeleteAuthURLParams)
66 |
67 | // now, add additional URL parameters defined in the check
68 | addAuthURLParams(authzURL, s.AuthURLParams)
69 |
70 | // set the redirect_uri value we will wait to be redirected to
71 | // if none was provided, this will default to the value in the redirect_uri URL parameter
72 | s.setExpectedRedirectURI()
73 |
74 | var err error
75 | switch s.FlowType {
76 | case oauth.FlowAuthorizationCode:
77 | s.AddDefaultExchangeParams()
78 | deleteRequiredExchangeParams(s.TokenExchangeParams, s.DeleteTokenExchangeParams)
79 | addTokenExchangeParams(s.TokenExchangeParams, s.TokenExchangeExtraParams)
80 | err = fi.DoAuthorizationRequest()
81 | if err != nil {
82 | s.errorMessage = err.Error()
83 | return warn, err
84 | }
85 |
86 | // if we were not redirected
87 | if fi.RedirectedToURL.String() == "" {
88 | s.failMessage = "Was not redirected during authorization code flow"
89 | return fail, nil
90 | }
91 |
92 | redirectedTo := fi.RedirectedToURL
93 | ok, err := s.requiredRedirectParamsPresent(redirectedTo)
94 | if !ok || err != nil {
95 | s.errorMessage = err.Error()
96 | return warn, err
97 | }
98 |
99 | authorizationCode := oauth.GetQueryParameterFirst(redirectedTo, oauth.AuthorizationCodeFlowResponseType)
100 | if authorizationCode == "" {
101 | s.failMessage = "Redirected without Authorization Code"
102 | return fail, nil
103 | }
104 |
105 | // set authorization code from redirect uri
106 | s.TokenExchangeParams[oauth.AuthorizationCodeFlowResponseType] = []string{authorizationCode}
107 | // perform exchange
108 | tok, err := fi.Exchange(context.TODO(), s.TokenExchangeParams)
109 |
110 | if err != nil {
111 | s.errorMessage = err.Error()
112 | return warn, err
113 | }
114 | if err == nil && len(tok.AccessToken) > 0 {
115 | return pass, nil
116 | }
117 |
118 | return fail, nil
119 |
120 | case oauth.FlowImplicit:
121 | err = fi.DoAuthorizationRequest()
122 | // this will only be set with a value
123 | // if we were redirected to the provided redirect_uri
124 | // therefore, if this is not empty, we were redirected
125 | // to the malicious URI
126 | if err != nil {
127 | s.errorMessage = err.Error()
128 | return warn, err
129 | }
130 |
131 | redirectedTo := fi.RedirectedToURL
132 | // if we were not redirected
133 | if redirectedTo.String() == "" {
134 | s.failMessage = "Was not redirected during implicit flow"
135 | return fail, nil
136 | }
137 |
138 | if oauth.GetImplicitAccessTokenFromURL(redirectedTo.String()) == "" {
139 | s.failMessage = "Redirected without Access Token"
140 | return fail, nil
141 | }
142 |
143 | ok, err := s.requiredRedirectParamsPresent(redirectedTo)
144 | if !ok || err != nil {
145 | s.errorMessage = err.Error()
146 | return warn, err
147 | }
148 |
149 | return pass, nil
150 | }
151 |
152 | // should never get here
153 | log.Fatalf("Received bad flowtype: %s\n", s.FlowType)
154 | s.errorMessage = "Something went wrong"
155 | return warn, errors.New("Something went wrong")
156 | }
157 |
158 | // Chrome checks if implicit flow tests pass by if we are redirected
159 | // to the expected redirect URI without an error. This sets
160 | // which redirect URI we should be waiting to be redirected to.
161 | func (s *step) setExpectedRedirectURI() {
162 | if len(s.WaitForRedirectTo) > 0 {
163 | // if we have specifically set the parameter in checks.json
164 | // to have a URL we are waiting to be redirected to
165 | // this is useful for cases where, for example, we provide
166 | // two redirect_uri parameters (one valid and one invalid) as part of a test.
167 | maliciousRedirectURI, err := url.Parse(s.WaitForRedirectTo)
168 | if err != nil {
169 | log.Fatalf("Bad WaitForRedirectTo value\n")
170 | }
171 | s.FlowInstance.ProvidedRedirectURL = maliciousRedirectURI
172 | } else {
173 | ur := s.FlowInstance.AuthorizationURL
174 | // addAuthURLPArams() is called before this, so we can search for the
175 | // redirect_uri parameter in the URL in the normal case
176 | redirectURIStr := oauth.GetQueryParameterFirst(ur, oauth.RedirectURIParam)
177 | redirectURI, err := url.Parse(redirectURIStr)
178 | if err != nil {
179 | log.Fatalf("Bad redirect_uri param\n")
180 | }
181 | s.FlowInstance.ProvidedRedirectURL = redirectURI
182 | }
183 | }
184 |
185 | // Add URL parameter to authorization URL. If the parameter already
186 | // exists in the URL, this will add an additional.
187 | func addAuthURLParams(authzURL *url.URL, pm map[string][]string) {
188 | for key, values := range pm {
189 | for _, v := range values {
190 | oauth.AddQueryParameter(authzURL, key, v)
191 | }
192 | }
193 | }
194 |
195 | // Delete required parameters that are
196 | // specified to be manually deleted. Parameters should always
197 | // be deleted before new ones are added.
198 | // The following parameters are required and would need
199 | // to be deleted if desired: state, redirect_uri, client_id, scope, response_type
200 | func deleteRequiredParams(authzURL *url.URL, p []string) {
201 | for _, d := range p {
202 | oauth.DelQueryParameter(authzURL, d)
203 | }
204 | }
205 |
206 | func (s *step) AddDefaultExchangeParams() {
207 | v := url.Values{
208 | "grant_type": {"authorization_code"},
209 | "redirect_uri": {s.FlowInstance.ProvidedRedirectURL.String()},
210 | }
211 | s.TokenExchangeParams = v
212 |
213 | }
214 |
215 | func addTokenExchangeParams(v url.Values, pm map[string][]string) {
216 | for key, values := range pm {
217 | if len(v[key]) == 0 {
218 | v[key] = values
219 | } else {
220 | v[key] = append(v[key], values...)
221 | }
222 | }
223 | }
224 |
225 | func deleteRequiredExchangeParams(v url.Values, p []string) {
226 | for _, d := range p {
227 | delete(v, d)
228 | }
229 | }
230 |
231 | // Checks if the URL we were redirected to contains the
232 | // parameters defined in the step that it must contain
233 | // Checks RedirectMustContainFragment for implicit flow and
234 | // RedirectMustContainURL for authorization code flow
235 | func (s *step) requiredRedirectParamsPresent(redirectedTo *url.URL) (bool, error) {
236 | var getParamFunc func(*url.URL, string) []string
237 | var requiredParams map[string][]string
238 | switch s.FlowType {
239 | case oauth.FlowAuthorizationCode:
240 | // If authz code flow, look at query params
241 | getParamFunc = oauth.GetQueryParameterAll
242 | requiredParams = s.RedirectMustContainURL
243 | case oauth.FlowImplicit:
244 | // If implicit flow, look at fragment params (parameters after "#")
245 | getParamFunc = oauth.GetFragmentParameterAll
246 | requiredParams = s.RedirectMustContainFragment
247 | default:
248 | return false, errors.New("Bad flow type")
249 | }
250 |
251 | for key, values := range requiredParams {
252 | redirectURLVals := getParamFunc(redirectedTo, key)
253 | for _, v := range values {
254 | if !sliceContains(redirectURLVals, v) {
255 | return false, fmt.Errorf("Missing value %s for key %s", v, key)
256 | }
257 | }
258 | }
259 | return true, nil
260 | }
261 |
262 | // checks if slice of strings contains given string
263 | func sliceContains(list []string, element string) bool {
264 | for _, item := range list {
265 | if item == element {
266 | return true
267 | }
268 | }
269 | return false
270 | }
271 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "os"
6 |
7 | "github.com/morganc3/KOAuth/browser"
8 | "github.com/morganc3/KOAuth/checks"
9 | "github.com/morganc3/KOAuth/config"
10 | )
11 |
12 | // Execute - Parse CLI flags, OAuth configuration file,
13 | // initialize browser session, and begin performing checks
14 | func Execute() {
15 | config.CliFlags.InitCliFlags() // Initialize and Parse CLI Flags
16 |
17 | cancel := browser.InitChromeSession() // Initialize Chrome browser configuration
18 | defer cancel()
19 |
20 | config.OAuthConfig.Init() // Parse OAuth configuration file provided
21 |
22 | // first tab's context and CancelFunc
23 | // this will be the first window, which
24 | // sets up authentication to the authorization server
25 | fctx, fctxCancel := initSession(config.GetOpt(config.FlagAuthenticationURL))
26 | defer fctxCancel()
27 |
28 | checkFile := config.GetOpt(config.FlagChecks)
29 | promptFlag := config.GetOpt(config.FlagPrompt)
30 | outDir := config.GetOpt(config.FlagOut)
31 | reportTemplate := config.GetOpt(config.FlagReportTemplate)
32 | performChecks(fctx, checkFile, promptFlag, outDir, reportTemplate)
33 | }
34 |
35 | func performChecks(ctx context.Context, checkFile, promptFlag, outDir, htmlReportTemplate string) {
36 | checks.Init(ctx, checkFile, promptFlag)
37 | checks.DoChecks()
38 | checks.PrintResults()
39 | checks.WriteResults(outDir, htmlReportTemplate)
40 | }
41 |
42 | func fileExists(path string) bool {
43 | if _, err := os.Stat(path); os.IsNotExist(err) {
44 | // file doesn't exist
45 | return false
46 | }
47 | return true
48 | }
49 |
--------------------------------------------------------------------------------
/cmd/session_init.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "fmt"
7 | "log"
8 | "os"
9 |
10 | "github.com/chromedp/chromedp"
11 | "github.com/morganc3/KOAuth/browser"
12 | "github.com/morganc3/KOAuth/oauth"
13 | )
14 |
15 | // Initialize session by navigating to the Authorization URL and logging in
16 | // This will wait until we are redirected to the correct redirect_uri
17 | // or the context times out. The purpose of this initialization is to
18 | // setup cookies, localstorage, indexdb, etc. in the browser.
19 |
20 | func initSession(authURL string) (context.Context, context.CancelFunc) {
21 | ctx, cancel := chromedp.NewContext(browser.ChromeExecContext)
22 |
23 | // if an authUrl was provided, auth there and return. Otherwise we
24 | // will do an oauth flow which should prompt the user to authenticate
25 | if authURL != "" {
26 | waitForAuth(ctx, authURL)
27 | return ctx, cancel
28 | }
29 |
30 | // TODO: some servers might not support implicit flow
31 | // for initializing/authenticating, we should probably at minimum
32 | // allow the option to do authorization code flow here in case
33 | // implicit flow will cause an immediate error. Otherwise, can simply
34 | // provide a url to authenticate to, and the user can in some way signal
35 | // when they have authenticated
36 |
37 | // We should be prompted for auth as this is our first request
38 | i := oauth.NewInstance(ctx, cancel, oauth.ImplicitFlowResponseType, "DONT_SEND")
39 |
40 | urlString := i.AuthorizationURL.String()
41 |
42 | // adds listener listening for a redirect to our redirect_uri
43 | ch := browser.WaitRedirect(ctx, i.ProvidedRedirectURL.Host, i.ProvidedRedirectURL.Path)
44 |
45 | err := chromedp.Run(ctx, chromedp.Navigate(urlString))
46 | if err != nil {
47 | log.Fatal(err)
48 | }
49 |
50 | select {
51 | case <-ctx.Done():
52 | log.Fatal("Context was cancelled")
53 | case urlstr := <-ch:
54 | i.RedirectedToURL = urlstr
55 | err = i.GetURLError() // get error as defined in rfc6749
56 | if err != nil {
57 | log.Fatal(err)
58 | }
59 | log.Println("Successfully authenticated")
60 | }
61 | return ctx, cancel
62 | }
63 |
64 | // Waits for the user to authenticate in the browser
65 | func waitForAuth(ctx context.Context, urlString string) {
66 | err := chromedp.Run(ctx, chromedp.Navigate(urlString))
67 | if err != nil {
68 | log.Fatal(err)
69 | }
70 |
71 | fmt.Println("Waiting for you to authenticate.")
72 | fmt.Print("Press enter when you have successfully authenticated: ")
73 | input := bufio.NewScanner(os.Stdin)
74 | input.Scan()
75 | fmt.Println("Successfully authenticated")
76 | }
77 |
--------------------------------------------------------------------------------
/config-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "redirect_url": "https://example.com/callback",
3 | "client_id": "12838298jdfusj87h38278",
4 | "client_secret": "asdjasd8asj8asdj",
5 | "scopes":["profile", "email"],
6 | "endpoint": {
7 | "auth_url": "https://accounts.google.com/o/oauth2/auth",
8 | "token_url": "https://oauth2.googleapis.com/token"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/config/cli_options.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "log"
5 | "os"
6 | "strconv"
7 |
8 | flag "github.com/ogier/pflag"
9 | )
10 |
11 | type cliFlag struct {
12 | name string
13 | hint string
14 | defaultValue string
15 | value *string
16 | }
17 |
18 | type cliFlagsMap map[string]*cliFlag
19 |
20 | // CliFlags - map of cliFlags with their names and values
21 | var CliFlags cliFlagsMap
22 |
23 | // CLI Flag constant values
24 | const (
25 | FlagConfig = "config"
26 | FlagChecks = "checks"
27 | FlagOut = "out"
28 | FlagAuthenticationURL = "authentication-url"
29 | FlagProxy = "proxy"
30 | FlagUserAgent = "user-agent"
31 | FlagTimeout = "timeout"
32 | FlagPrompt = "prompt"
33 | FlagClientAuth = "client-auth"
34 | FlagReportTemplate = "report-template"
35 | )
36 |
37 | // InitCliFlags - Initialize CliFlagsMap and parse CLI flags
38 | func (c *cliFlagsMap) InitCliFlags() {
39 | *c = make(cliFlagsMap)
40 |
41 | c.newFlag(FlagConfig, "input oauth configuration file", "config.json")
42 | c.newFlag(FlagChecks, "file containing checks to run", "./checks/rules/checks.json")
43 | c.newFlag(FlagOut, "directory for output to be stored", "output/")
44 | c.newFlag(FlagAuthenticationURL,
45 | `Url to originally authenticate at to establish an authenticated session in the browser.
46 | If left blank, authentication will occur through an OAuth flow.`,
47 | "")
48 | c.newFlag(FlagProxy, "HTTP Proxy :", "")
49 | c.newFlag(FlagUserAgent, "User-Agent Header for Chrome", `Chrome`)
50 | c.newFlag(FlagTimeout, "Timeout for waiting for OAuth redirects to redirect_uri", "4")
51 | c.newFlag(FlagPrompt, `Value of "prompt" parameter in authorization request. If the authorization
52 | server does not support prompt=none, it should be set to "login" or "select_account". If the
53 | pressence of the prompt parameter breaks the flow, set to this flag to the string "DONT_SEND"
54 | and it will not be sent.`, "none")
55 | c.newFlag(FlagClientAuth, `Client Authentication Method: "BASIC", "BODY", or "auto", to indicate if
56 | client ID and client secret should be sent in an HTTP Basic authentication header or in the POST body,
57 | or should be auto detected.`, "auto")
58 | c.newFlag(FlagReportTemplate, "HTML report template to consume JSON output", "./checks/assets/report.html")
59 |
60 | c.parseCliFlags() // parse CLI flags
61 | filePathsExist() // ensure file paths provided by CLI flags exist
62 | }
63 |
64 | func (c cliFlagsMap) newFlag(name, hint string, defaultValue string) {
65 | f := cliFlag{
66 | name: name,
67 | hint: hint,
68 | defaultValue: defaultValue,
69 | }
70 | c[name] = &f
71 | }
72 |
73 | // parse cli flags, storing in CliFlagsMap
74 | func (c cliFlagsMap) parseCliFlags() {
75 | for _, v := range c {
76 | c.parseFlag(v)
77 | }
78 | flag.Parse()
79 |
80 | }
81 |
82 | func (c cliFlagsMap) parseFlag(cf *cliFlag) {
83 | val := flag.String(cf.name, cf.defaultValue, cf.hint)
84 | c[cf.name].value = val
85 | }
86 |
87 | // GetOpt - get cli option value
88 | func GetOpt(name string) string {
89 | return *CliFlags[name].value
90 | }
91 |
92 | // GetOptAsInt - get cli option as int
93 | func GetOptAsInt(name string) int {
94 | v, err := strconv.Atoi(*CliFlags[name].value)
95 | if err != nil {
96 | log.Fatalf("Bad option value - could not be converted to int\n")
97 | }
98 | return v
99 | }
100 |
101 | // Must only be called after flags have been parsed
102 | func filePathsExist() {
103 | // ensure input check JSON file exists
104 | checkFile := GetOpt(FlagChecks)
105 | if !fileExists(checkFile) {
106 | log.Printf("Check file at %s does not exist\n", checkFile)
107 | log.Fatal("The default check file is in the repository at KOAuth/checks/rules/checks.json")
108 | }
109 |
110 | // ensure OAuth config file exists
111 | oauthConfig := GetOpt(FlagConfig)
112 | if !fileExists(oauthConfig) {
113 | log.Fatalf("OAuth configuration file at %s does not exist\n", oauthConfig)
114 | }
115 |
116 | // ensure HTML report template file exists
117 | reportTemplate := GetOpt(FlagReportTemplate)
118 | if !fileExists(reportTemplate) {
119 | log.Printf("HTML Report template file at %s does not exist\n", reportTemplate)
120 | log.Fatal("The default report template file is in the repository at KOAuth/checks/assets/report.html")
121 | }
122 | }
123 |
124 | func fileExists(path string) bool {
125 | if _, err := os.Stat(path); os.IsNotExist(err) {
126 | // file doesn't exist
127 | return false
128 | }
129 | return true
130 | }
131 |
--------------------------------------------------------------------------------
/config/oauth_config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "log"
7 | "net/url"
8 | "os"
9 |
10 | "golang.org/x/oauth2"
11 | )
12 |
13 | // OAuthConfig - KOAuth oauth config. different from Golang's oauth2 config object.
14 | var OAuthConfig kOAuthConfig
15 |
16 | type endpointWrapper struct {
17 | AuthURL string `json:"auth_url"`
18 | TokenURL string `json:"token_url"`
19 | }
20 |
21 | type oAuthConfigWrapper struct {
22 | ClientID string `json:"client_id"`
23 | ClientSecret string `json:"client_secret"`
24 | Endpoint endpointWrapper `json:"endpoint"`
25 | RedirectURL string `json:"redirect_url"`
26 | Scopes []string `json:"scopes"`
27 | }
28 |
29 | type kOAuthConfig struct {
30 | OAuth2Config oauth2.Config
31 | }
32 |
33 | // Get an oauth2 config from JSON file
34 | func readOAuthConfig(oauthConfigFile string, authStyle string) oauth2.Config {
35 | jsonFile, err := os.Open(oauthConfigFile)
36 | if err != nil {
37 | panic("Error opening oauth config file")
38 | }
39 | defer jsonFile.Close()
40 | byteValue, err := ioutil.ReadAll(jsonFile)
41 | if err != nil {
42 | panic("Error reading oauth config JSON file")
43 | }
44 | var conf oAuthConfigWrapper
45 | err = json.Unmarshal(byteValue, &conf)
46 | if err != nil {
47 | panic("Error unmarshalling oauth config")
48 | }
49 |
50 | var clientAuth oauth2.AuthStyle
51 | switch authStyle {
52 | case "BASIC":
53 | clientAuth = oauth2.AuthStyleInHeader
54 | case "BODY":
55 | clientAuth = oauth2.AuthStyleInParams
56 | default:
57 | clientAuth = oauth2.AuthStyleAutoDetect
58 | }
59 | var oauthConfig = &oauth2.Config{
60 | RedirectURL: conf.RedirectURL,
61 | ClientID: conf.ClientID,
62 | ClientSecret: conf.ClientSecret,
63 | Scopes: conf.Scopes,
64 | Endpoint: oauth2.Endpoint{
65 | AuthURL: conf.Endpoint.AuthURL,
66 | TokenURL: conf.Endpoint.TokenURL,
67 | AuthStyle: clientAuth,
68 | },
69 | }
70 | return *oauthConfig
71 | }
72 |
73 | func getHost(urlStr string) string {
74 | url, err := url.Parse(urlStr)
75 | if err != nil {
76 | log.Fatal(err)
77 | }
78 | return url.Host
79 | }
80 |
81 | func (c *kOAuthConfig) GetRedirectURIHost() string {
82 | return getHost(c.OAuth2Config.RedirectURL)
83 | }
84 |
85 | func (c *kOAuthConfig) GetConfigHost() string {
86 | return getHost(c.OAuth2Config.Endpoint.AuthURL)
87 | }
88 |
89 | func newConfig(oauthConfigFile, authStyle string) kOAuthConfig {
90 | conf := new(kOAuthConfig)
91 | conf.OAuth2Config = readOAuthConfig(oauthConfigFile, authStyle)
92 | return *conf
93 | }
94 |
95 | // Init - initialize KOAuthConfig object based on cli flags and oauth config file
96 | func (c *kOAuthConfig) Init() {
97 | configFile := GetOpt(FlagConfig)
98 | clientAuth := GetOpt(FlagClientAuth)
99 | OAuthConfig = newConfig(configFile, clientAuth)
100 | }
101 |
--------------------------------------------------------------------------------
/config/templating.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "log"
5 | "net/url"
6 |
7 | "github.com/hoisie/mustache"
8 | )
9 |
10 | // TODO: process skips here, add flag to skip?
11 |
12 | // GenerateChecksInput - Makes values from config file available to
13 | // checks so that check JSON input file can use
14 | // values, such as the domain of the redirect_uri
15 | // Supported keys: REDIRECT_URI, REDIRECT_SCHEME, REDIRECT_DOMAIN, REDIRECT_PATH,
16 | // CLIENT_ID, CLIENT_SECRET, SCOPES, AUTH_URL, TOKEN_URL
17 | func GenerateChecksInput(configFile string) []byte {
18 | templateKeyMap := make(map[string]interface{})
19 | redirectURI, err := url.Parse(OAuthConfig.OAuth2Config.RedirectURL)
20 | if err != nil {
21 | log.Fatal("invalid redirect_uri provided")
22 | }
23 |
24 | templateKeyMap["REDIRECT_URI"] = redirectURI.String()
25 | templateKeyMap["REDIRECT_SCHEME"] = redirectURI.Scheme
26 | templateKeyMap["REDIRECT_DOMAIN"] = redirectURI.Host
27 | templateKeyMap["REDIRECT_PATH"] = redirectURI.Path
28 | templateKeyMap["CLIENT_ID"] = OAuthConfig.OAuth2Config.ClientID
29 | templateKeyMap["CLIENT_SECRET"] = OAuthConfig.OAuth2Config.ClientSecret
30 | templateKeyMap["SCOPES"] = OAuthConfig.OAuth2Config.Scopes
31 | templateKeyMap["AUTH_URL"] = OAuthConfig.OAuth2Config.Endpoint.AuthURL
32 | templateKeyMap["TOKEN_URL"] = OAuthConfig.OAuth2Config.Endpoint.TokenURL
33 |
34 | data := mustache.RenderFile(configFile, templateKeyMap)
35 | bytes := []byte(data)
36 | return bytes
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/docs/KOAuth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/morganc3/KOAuth/ae642e283f0426d6b824cb9742123c133604b777/docs/KOAuth.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/morganc3/KOAuth
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/chromedp/cdproto v0.0.0-20200709115526-d1f6fc58448b
7 | github.com/chromedp/chromedp v0.5.4-0.20200729192944-ccb1bb06c868
8 | github.com/go-chi/chi v4.1.2+incompatible
9 | github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 // indirect
10 | github.com/gobwas/pool v0.2.1 // indirect
11 | github.com/gobwas/ws v1.0.4 // indirect
12 | github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69
13 | github.com/mailru/easyjson v0.7.6 // indirect
14 | github.com/ogier/pflag v0.0.1
15 | github.com/stretchr/testify v1.4.0
16 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
17 | golang.org/x/net v0.0.0-20201021035429-f5854403a974
18 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
19 | golang.org/x/tools v0.1.0 // indirect
20 | honnef.co/go/tools v0.0.1-2020.1.4
21 | )
22 |
23 | replace golang.org/x/oauth2 => github.com/morganc3/oauth2 v0.1.12
24 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
15 | cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
17 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
18 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
19 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
20 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
21 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
22 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
23 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
24 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
25 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
26 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
27 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
28 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
29 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
30 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
31 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
32 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
33 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
34 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
35 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
36 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
37 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
38 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
39 | github.com/chromedp/cdproto v0.0.0-20200116234248-4da64dd111ac/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
40 | github.com/chromedp/cdproto v0.0.0-20200709115526-d1f6fc58448b h1:LF+GRwyzxrO3MUzPvejv+yBup0lNG+/QdIRrkxOPseA=
41 | github.com/chromedp/cdproto v0.0.0-20200709115526-d1f6fc58448b/go.mod h1:E6LPWRdIJc11h/di5p0rwvRmUYbhGpBEH7ZbPfzDIOE=
42 | github.com/chromedp/chromedp v0.5.3 h1:F9LafxmYpsQhWQBdCs+6Sret1zzeeFyHS5LkRF//Ffg=
43 | github.com/chromedp/chromedp v0.5.3/go.mod h1:YLdPtndaHQ4rCpSpBG+IPpy9JvX0VD+7aaLxYgYj28w=
44 | github.com/chromedp/chromedp v0.5.4-0.20200729192944-ccb1bb06c868 h1:p7PppOWSzj/rLAFo5I58qM5wOQaDfFOaJ8hn+uPzaM8=
45 | github.com/chromedp/chromedp v0.5.4-0.20200729192944-ccb1bb06c868/go.mod h1:qyppbPfbdDXbhCiOluIIp3cIZf5t4e6OxIGTBEfu/t8=
46 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
47 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
48 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
49 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
50 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
51 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
52 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
53 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
54 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
55 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
56 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
57 | github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
58 | github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
59 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
60 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
61 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
62 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
63 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
64 | github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 h1:YyrUZvJaU8Q0QsoVo+xLFBgWDTam29PKea6GYmwvSiQ=
65 | github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
66 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
67 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
68 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
69 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
70 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
71 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
72 | github.com/gobwas/ws v1.0.3/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
73 | github.com/gobwas/ws v1.0.4 h1:5eXU1CZhpQdq5kXbKb+sECH5Ia5KiO6CYzIzdlVx6Bs=
74 | github.com/gobwas/ws v1.0.4/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
75 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
76 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
77 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
78 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
79 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
80 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
81 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
82 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
83 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
84 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
85 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
86 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
87 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
88 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
89 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
90 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
91 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
92 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
93 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
94 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
95 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
96 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
97 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
98 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
99 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
100 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
101 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
102 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
103 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
104 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
105 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
106 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
107 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
108 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
109 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
110 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
111 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
112 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
113 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
114 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
115 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
116 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
117 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
118 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
119 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
120 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
121 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
122 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
123 | github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69 h1:umaj0TCQ9lWUUKy2DxAhEzPbwd0jnxiw1EI2z3FiILM=
124 | github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69/go.mod h1:zdLK9ilQRSMjSeLKoZ4BqUfBT7jswTGF8zRlKEsiRXA=
125 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
126 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
127 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
128 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
129 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
130 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
131 | github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
132 | github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
133 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
134 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
135 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
136 | github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
137 | github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8=
138 | github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
139 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
140 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
141 | github.com/morganc3/oauth2 v0.0.0-20201230233733-a7c1eea8f22b h1:q3HaS0n4TJfUnfq2InQ6pOfsFp+Njfn2iuETLCsX3Eg=
142 | github.com/morganc3/oauth2 v0.0.0-20201230233733-a7c1eea8f22b/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
143 | github.com/morganc3/oauth2 v0.1.0 h1:h21hyR6EhN76CoAmLyht3N4rIJDYocNqxRm6ODI/qo4=
144 | github.com/morganc3/oauth2 v0.1.0/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
145 | github.com/morganc3/oauth2 v0.1.1 h1:qPN9EsCI2Bk5MEvmcqdpUh/reCGl3J9n1pRb0TK8EEg=
146 | github.com/morganc3/oauth2 v0.1.1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
147 | github.com/morganc3/oauth2 v0.1.2 h1:6ktcD7Tsv3ddifXmXpRpxz3eBtzPBmD60UAWiOCkq08=
148 | github.com/morganc3/oauth2 v0.1.2/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
149 | github.com/morganc3/oauth2 v0.1.3 h1:rxuxTo6zWkgf73woBmbSQpwexQyTIFGS6/UVYgMsjsY=
150 | github.com/morganc3/oauth2 v0.1.3/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
151 | github.com/morganc3/oauth2 v0.1.6 h1:zGv/DXYA4FqK3izAz+Z7pX/IGI6LtWgQsRkKzLgRQ3U=
152 | github.com/morganc3/oauth2 v0.1.6/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
153 | github.com/morganc3/oauth2 v0.1.7 h1:IXE3QqRBK18vxsF3RCY09hLX2w+aZVfc7cvtakSbl48=
154 | github.com/morganc3/oauth2 v0.1.7/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
155 | github.com/morganc3/oauth2 v0.1.8 h1:yVjmYYYnm3JElK37CR0vIYZzfPHSIk2nEcZrO5etPPk=
156 | github.com/morganc3/oauth2 v0.1.8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
157 | github.com/morganc3/oauth2 v0.1.9 h1:redtjBv4al6yYt4ZOElu4RIeL8YCSoBzrBlYW/gSTTY=
158 | github.com/morganc3/oauth2 v0.1.9/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
159 | github.com/morganc3/oauth2 v0.1.10 h1:XL1ynqtrO2QT2GJzCceTss6DFQYQA0A24xXIzEBMmEs=
160 | github.com/morganc3/oauth2 v0.1.10/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
161 | github.com/morganc3/oauth2 v0.1.11 h1:4PINR5IyLpNwLX0XlbvfJ4jktCW/UaC1mVkGjmS4hBA=
162 | github.com/morganc3/oauth2 v0.1.11/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
163 | github.com/morganc3/oauth2 v0.1.12 h1:dzxK/OOCOgKe/idw1u6Psk4z1qS74ZSAjmF52rtkYeo=
164 | github.com/morganc3/oauth2 v0.1.12/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
165 | github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750=
166 | github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g=
167 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
168 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
169 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
170 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
171 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
172 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
173 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
174 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
175 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
176 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
177 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
178 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
179 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
180 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
181 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
182 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
183 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
184 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
185 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
186 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
187 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
188 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
189 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
190 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
191 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
192 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
193 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
194 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
195 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
196 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
197 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
198 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
199 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
200 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
201 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
202 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
203 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
204 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
205 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
206 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
207 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
208 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
209 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
210 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
211 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
212 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
213 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
214 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
215 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
216 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
217 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
218 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
219 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
220 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
221 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
222 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
223 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
224 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
225 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
226 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
227 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
228 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
229 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
230 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
231 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
232 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
233 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
234 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
235 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
236 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
237 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
238 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
239 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
240 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
241 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
242 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
243 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
244 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
245 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
246 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
247 | golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
248 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
249 | golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
250 | golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
251 | golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
252 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
253 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
254 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
255 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
256 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
257 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
258 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
259 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
260 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
261 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
262 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
263 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
264 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
265 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
266 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
267 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
268 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
269 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
270 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
271 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
272 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
273 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
274 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
275 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
276 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
277 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
278 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
279 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
280 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
281 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
282 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
283 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
284 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
285 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
286 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
287 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
288 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
289 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
290 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
291 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
292 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
293 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
294 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
295 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
296 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
297 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
298 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
299 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
300 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
301 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
302 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
303 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
304 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
305 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
306 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
307 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
308 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
309 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
310 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
311 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
312 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
313 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
314 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
315 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
316 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
317 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
318 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
319 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
320 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
321 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
322 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
323 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
324 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
325 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
326 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
327 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
328 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
329 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
330 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
331 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
332 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
333 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
334 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
335 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
336 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
337 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
338 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
339 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
340 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
341 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
342 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
343 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
344 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
345 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
346 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k=
347 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
348 | golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
349 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
350 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
351 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
352 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
353 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
354 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
355 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
356 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
357 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
358 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
359 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
360 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
361 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
362 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
363 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
364 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
365 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
366 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
367 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
368 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
369 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
370 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
371 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
372 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
373 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
374 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
375 | google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
376 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
377 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
378 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
379 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
380 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
381 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
382 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
383 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
384 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
385 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
386 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
387 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
388 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
389 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
390 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
391 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
392 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
393 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
394 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
395 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
396 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
397 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
398 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
399 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
400 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
401 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
402 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
403 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
404 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
405 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
406 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
407 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
408 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
409 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
410 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
411 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
412 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
413 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
414 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
415 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
416 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
417 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
418 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
419 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
420 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
421 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
422 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
423 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
424 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
425 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
426 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
427 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
428 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
429 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
430 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
431 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
432 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
433 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
434 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
435 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
436 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
437 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
438 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
439 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
440 | honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
441 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
442 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
443 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
444 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
445 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/morganc3/KOAuth/cmd"
5 | )
6 |
7 | func main() {
8 | cmd.Execute()
9 | }
10 |
--------------------------------------------------------------------------------
/oauth/errors.go:
--------------------------------------------------------------------------------
1 | package oauth
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | )
7 |
8 | // error constants
9 | const (
10 | ContextTimeoutError = "context deadline exceeded"
11 | ContextCancelledError = "context canceled"
12 | NotRedirectedError = "Browser was never redirected to the provided redirect_uri"
13 | )
14 |
15 | // GetURLError - gets error from URL parameter as defined in the OAuth 2.0 specification
16 | func (i *FlowInstance) GetURLError() error {
17 | if i.RedirectedToURL == nil {
18 | return errors.New(NotRedirectedError)
19 | }
20 |
21 | errorType := GetFragmentParameterFirst(i.RedirectedToURL, "error")
22 | errorDescription := GetFragmentParameterFirst(i.RedirectedToURL, "error_description")
23 |
24 | if len(errorType) > 0 || len(errorDescription) > 0 {
25 | return fmt.Errorf("%s: %s", errorType, errorDescription)
26 | }
27 | return nil
28 | }
29 |
--------------------------------------------------------------------------------
/oauth/oauth_constants.go:
--------------------------------------------------------------------------------
1 | package oauth
2 |
3 | // OAuth 2.0 HTTP parameter constants
4 | const (
5 | RedirectURIParam = "redirect_uri"
6 | ResponseTypeParam = "response_type"
7 | GrantTypeParam = "grant_type"
8 | StateParam = "state"
9 | TokenTypeParam = "token_type"
10 | ExpiresInParam = "expires_in"
11 | ScopeParam = "scope"
12 | RefreshTokenParam = "refresh_token"
13 | AccessTokenParam = "access_token"
14 | ClientIDParam = "client_id"
15 | ClientSecretParam = "client_secret"
16 | UsernameParam = "username"
17 | PasswordParam = "password"
18 | ErrorParam = "error"
19 | PKCECodeVerifierParam = "code_verifier"
20 | PKCECodeChallengeParam = "code_challenge"
21 | PKCECodeChallengeMethodParam = "code_challenge_method"
22 | )
23 |
24 | // PKCE code challenge method param values
25 | const (
26 | PKCES256 = "S256"
27 | PKCEPLAIN = "plain"
28 | )
29 |
30 | // OAuth 2.0 flow types, as defined in provided JSON check structure
31 | const (
32 | FlowAuthorizationCode = "authorization-code"
33 | FlowImplicit = "implicit"
34 | )
35 |
--------------------------------------------------------------------------------
/oauth/oauth_flow.go:
--------------------------------------------------------------------------------
1 | package oauth
2 |
3 | import (
4 | "context"
5 | "crypto/rand"
6 | "encoding/base64"
7 | "log"
8 | "net/http"
9 | "net/http/httputil"
10 | "net/url"
11 | "time"
12 |
13 | "github.com/chromedp/chromedp"
14 | "github.com/morganc3/KOAuth/browser"
15 | "github.com/morganc3/KOAuth/config"
16 | "golang.org/x/oauth2"
17 | )
18 |
19 | // FlowType TODO: FlowType should really be "ResponseType" to be more accurate,
20 | // this will also clear up confusion between FlowType in Check struct
21 | type FlowType string
22 |
23 | // Response types
24 | const (
25 | ImplicitFlowResponseType = "token"
26 | AuthorizationCodeFlowResponseType = "code"
27 | )
28 |
29 | // FlowInstance - Represents an instance of an OAuth 2.0 Flow
30 | type FlowInstance struct {
31 | FlowType FlowType `json:"-"`
32 | FlowTimeoutSeconds time.Duration `json:"-"`
33 | Ctx context.Context `json:"-"`
34 | Cancel context.CancelFunc `json:"-"`
35 | AuthorizationURL *url.URL `json:"-"`
36 | ProvidedRedirectURL *url.URL `json:"-"`
37 | RedirectedToURL *url.URL `json:"-"`
38 | ExchangeRequest *ExchangeRequest `json:"exchangeRequest,omitempty"`
39 | }
40 |
41 | // ExchangeRequest - Represents an authorization code exchange request for an Access Token
42 | type ExchangeRequest struct {
43 | RequestString string `json:"request,omitempty"`
44 | ResponseString string `json:"response,omitempty"`
45 | Request *http.Request `json:"-"`
46 | Response *http.Response `json:"-"`
47 | }
48 |
49 | // TODO: There are likely to be applications where
50 | // either:
51 | // A. Session information is updated on each request
52 | // and the previous value is invalidated
53 | // B. Session information is cleared when an
54 | // error / authz issue occurs.
55 | // Support should be added to support both of these cases
56 |
57 | // Scenario A is accounted for currently, as we use the same
58 | // chrome context for each check.
59 |
60 | // NewInstance - Creates a new OAuth flow instance
61 | func NewInstance(cx context.Context, cancel context.CancelFunc, ft FlowType, promptFlag string) *FlowInstance {
62 | redirectURI, err := url.Parse(config.OAuthConfig.OAuth2Config.RedirectURL)
63 | if err != nil {
64 | log.Fatalf("Failed to parse provided redirect_uri in config file")
65 | }
66 | flowInstance := FlowInstance{
67 | FlowType: ft,
68 | FlowTimeoutSeconds: time.Duration(config.GetOptAsInt(config.FlagTimeout)),
69 | ProvidedRedirectURL: redirectURI,
70 | RedirectedToURL: new(url.URL),
71 | Ctx: cx,
72 | Cancel: cancel,
73 | }
74 | flowInstance.AuthorizationURL = GenerateAuthorizationURL(ft, "random_state_value", promptFlag)
75 |
76 | return &flowInstance
77 | }
78 |
79 | // DoAuthorizationRequest - Perform OAuth 2.0 authorization request
80 | // based on the values from the oauth config.
81 | // Waits for redirect to expected redirect URL
82 | func (i *FlowInstance) DoAuthorizationRequest() error {
83 | var actions []chromedp.Action
84 |
85 | urlString := i.AuthorizationURL.String()
86 |
87 | actions = append(actions, chromedp.Navigate(urlString))
88 | // adds listener which will cancel the context
89 | // if a redirect to redirect_uri occurs
90 | ch := browser.WaitRedirect(i.Ctx, i.ProvidedRedirectURL.Host, i.ProvidedRedirectURL.Path)
91 | c, err := browser.RunWithTimeOut(&i.Ctx, time.Duration(config.GetOptAsInt(config.FlagTimeout)), actions)
92 | if err != nil {
93 | return err
94 | }
95 |
96 | select {
97 | case <-c.Done():
98 | return err
99 | case urlstr := <-ch:
100 | i.RedirectedToURL = urlstr
101 | err = i.GetURLError() // get error as defined in rfc6749
102 | if err != nil {
103 | return err
104 | }
105 | }
106 |
107 | return err
108 |
109 | }
110 |
111 | // Exchange - uses forked version of oauth2 package
112 | // Same as Exchange() from https://github.com/golang/oauth2 but
113 | // takes arbitrary url values and gives access to HTTP request and response
114 | func (i *FlowInstance) Exchange(ctx context.Context, v url.Values) (*oauth2.Token, error) {
115 | req, resp, tkn, err := oauth2.RetrieveToken(ctx, &config.OAuthConfig.OAuth2Config, v)
116 | var reqString, respString string
117 | if req != nil {
118 | reqBytes, err := httputil.DumpRequest(req, true)
119 | reqString = string(reqBytes)
120 | if err != nil {
121 | log.Println(err)
122 | }
123 | }
124 | if resp != nil {
125 | respBytes, err := httputil.DumpResponse(resp, true)
126 | respString = string(respBytes)
127 | if err != nil {
128 | log.Println(err)
129 | }
130 | }
131 |
132 | i.ExchangeRequest = &ExchangeRequest{
133 | Request: req,
134 | Response: resp,
135 | RequestString: reqString,
136 | ResponseString: respString,
137 | }
138 | return tkn, err
139 |
140 | }
141 |
142 | // GenerateAuthorizationURL - generates oauth2 authorization url based on config values
143 | func GenerateAuthorizationURL(flowType FlowType, state, promptFlag string) *url.URL {
144 | var option oauth2.AuthCodeOption = oauth2.SetAuthURLParam(ResponseTypeParam, string(flowType))
145 | URLString := config.OAuthConfig.OAuth2Config.AuthCodeURL(state, option)
146 | URL, err := url.Parse(URLString)
147 | if err != nil {
148 | log.Fatal(err)
149 | }
150 |
151 | switch promptFlag {
152 | case "DONT_SEND":
153 | default:
154 | SetQueryParameter(URL, "prompt", promptFlag)
155 | }
156 |
157 | // some authz servers (such as Okta) require a Nonce
158 | // despite it not being part of the RFC
159 | SetQueryParameter(URL, "nonce", randStr(32))
160 | return URL
161 | }
162 |
163 | // UpdateFlowType - update FlowType value and update Authorization URL
164 | func (i *FlowInstance) UpdateFlowType(ft string) {
165 | var responeType string
166 | switch ft {
167 | case FlowImplicit:
168 | responeType = ImplicitFlowResponseType
169 | case FlowAuthorizationCode:
170 | responeType = AuthorizationCodeFlowResponseType
171 | }
172 |
173 | i.FlowType = FlowType(responeType)
174 | SetQueryParameter(i.AuthorizationURL, ResponseTypeParam, responeType)
175 | }
176 |
177 | // GetImplicitAccessTokenFromURL gets access token from URL fragment
178 | // during implicit flow
179 | func GetImplicitAccessTokenFromURL(urlString string) string {
180 | u, err := url.Parse(urlString)
181 | if err != nil {
182 | log.Fatal(err)
183 | }
184 |
185 | values, _ := url.ParseQuery(u.Fragment)
186 | tokenString := values.Get(AccessTokenParam)
187 | return tokenString
188 | }
189 |
190 | func randStr(len int) string {
191 | buff := make([]byte, len)
192 | rand.Read(buff)
193 | str := base64.URLEncoding.EncodeToString(buff)
194 | // Base 64 can be longer than len
195 | return str[:len]
196 | }
197 |
--------------------------------------------------------------------------------
/oauth/oauth_flow_test.go:
--------------------------------------------------------------------------------
1 | package oauth
2 |
3 | import (
4 | "context"
5 | "net/url"
6 | "testing"
7 |
8 | "github.com/chromedp/chromedp"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestURLFunctions(t *testing.T) {
13 | ctx, cancel := chromedp.NewContext(context.Background())
14 | flow := NewInstance(ctx, cancel, ImplicitFlowResponseType, "none")
15 | flow.AuthorizationURL, _ = url.Parse("http://example.com")
16 | AddQueryParameter(flow.AuthorizationURL, "k1", "v1")
17 | assert.Equal(t, "http://example.com?k1=v1", flow.AuthorizationURL.String())
18 |
19 | AddQueryParameter(flow.AuthorizationURL, "k2", "v2")
20 | assert.Equal(t, "http://example.com?k1=v1&k2=v2", flow.AuthorizationURL.String())
21 |
22 | DelQueryParameter(flow.AuthorizationURL, "k1")
23 | assert.Equal(t, "http://example.com?k2=v2", flow.AuthorizationURL.String())
24 |
25 | DelQueryParameter(flow.AuthorizationURL, "k2")
26 | assert.Equal(t, "http://example.com", flow.AuthorizationURL.String())
27 |
28 | AddQueryParameter(flow.AuthorizationURL, "k1", "v1")
29 | SetQueryParameter(flow.AuthorizationURL, "k1", "newvalue")
30 | assert.Equal(t, "http://example.com?k1=newvalue", flow.AuthorizationURL.String())
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/oauth/url_utils.go:
--------------------------------------------------------------------------------
1 | package oauth
2 |
3 | import (
4 | "log"
5 | "net/url"
6 | )
7 |
8 | // SetQueryParameter - Sets value of the first key in the URL Query
9 | func SetQueryParameter(u *url.URL, key, value string) {
10 | q := u.Query()
11 | q.Set(key, value)
12 | u.RawQuery = q.Encode()
13 | }
14 |
15 | // AddQueryParameter - Adds a query parameter value. If a value already exists with
16 | // the specified key, this will add a second key/value pair in the URL
17 | func AddQueryParameter(u *url.URL, key, value string) {
18 | q := u.Query()
19 | if len(q[key]) == 0 {
20 | q[key] = []string{value}
21 | } else {
22 | q[key] = append(q[key], value)
23 | }
24 |
25 | u.RawQuery = q.Encode()
26 | }
27 |
28 | // GetFragmentParameterAll - Returns all values in the URL fragment
29 | func GetFragmentParameterAll(u *url.URL, key string) []string {
30 | values, err := url.ParseQuery(u.Fragment)
31 | if err != nil {
32 | log.Println("Error: Could not parse Fragment params")
33 | log.Println(err)
34 | return values[key]
35 | }
36 | return values[key]
37 | }
38 |
39 | // GetFragmentParameterFirst - Returns first instance of key in URL fragment
40 | func GetFragmentParameterFirst(u *url.URL, key string) string {
41 | values, err := url.ParseQuery(u.Fragment)
42 | if err != nil {
43 | log.Println("Error: Could not parse Fragment params")
44 | log.Println(err)
45 | return ""
46 | }
47 | return values.Get(key)
48 | }
49 |
50 | // GetQueryParameterAll - Returns all values in the URL query with the specified key
51 | func GetQueryParameterAll(u *url.URL, key string) []string {
52 | values := u.Query()[key]
53 | return values
54 | }
55 |
56 | // GetQueryParameterFirst - Get first instance of key in URL
57 | func GetQueryParameterFirst(u *url.URL, key string) string {
58 | return u.Query().Get(key)
59 | }
60 |
61 | // DelQueryParameter - Delete first instance of key pair in URL
62 | func DelQueryParameter(u *url.URL, key string) {
63 | q := u.Query()
64 | q.Del(key)
65 | u.RawQuery = q.Encode()
66 | }
67 |
--------------------------------------------------------------------------------