Authorization of a Twitch Extension
Tags: devjournal, security
Published: 2021-11-04 05:43:27

This article is the third in a multi-part series to walk through the creation of a Twitch extension. For the third part, the goal is to refactor the authorization.

To go directly to the project, the source code repository is available at {% github shrmpy/pavlok no-readme %} and {% github shrmpy/pavlok-panel no-readme %}


__Requirements:__
- CORS
- OAuth2
- encryption

## &sect; Overview

{% codepen https://codepen.io/shrmpy/pen/ExvvOmX %}

## &sect; Routes
### `/auth` flow
{% codepen https://codepen.io/shrmpy/pen/JjyJJRv %}

- There was claims-extraction processing in earlier incarnations as part of the `state` field construction. In examining the flow, the diagram helps show there is no longer a claims-extraction step. It's been isolated from `preprocess` which means now the logic that enforces the extension secret is missing. We have to decide how to re-enable the verification of the extension secret. This will be the focus of our first refactor effort.
- Also confirmed after diagramming, the Fauna data abstraction layer is not used. After double-checking via `grep`, it's safe to remove the `fauna.go` file.

{% codepen https://codepen.io/shrmpy/pen/qBXjmJP %}

{% codepen https://codepen.io/shrmpy/pen/dyzRRma %}



{% codepen https://codepen.io/shrmpy/pen/xxLrLMG %}

{% codepen https://codepen.io/shrmpy/pen/XWagzMe %}

{% codepen https://codepen.io/shrmpy/pen/PoKjOBq %}

{% codepen https://codepen.io/shrmpy/pen/xxLrpEe %}

### `/callback` flow
{% codepen https://codepen.io/shrmpy/pen/NWvvGMx %}

{% codepen https://codepen.io/shrmpy/pen/QWMMyyg %}

{% codepen https://codepen.io/shrmpy/pen/OJjjNaB %}

{% codepen https://codepen.io/shrmpy/pen/porrybG %}

{% codepen https://codepen.io/shrmpy/pen/OJjjXRY %}

{% codepen https://codepen.io/shrmpy/pen/oNeezgO %}

## &sect; Test
Start the refactor by adding [another test](https://gist.github.com/shrmpy/14638c7b525cb09e6cf7cae712c4892c) to the `main_test.go` [file](https://github.com/shrmpy/pavlok/blob/71d9db338da142d436707a590b8fc738b44be542/cmd/auth/main_test.go):
```go
func TestWrongExtensionSecret(t *testing.T) {
        // prepare data
        wrongHeader := make(map[string]string)
        wrongHeader["Authorization"] = "Bearer WRONG-KEY"
        req := events.APIGatewayProxyRequest{
                HTTPMethod: "GET",
                Body:       "",
                Headers:    wrongHeader,
        }

        // run request handlerlogic
        result, err := handler(req)
        assert.IsType(t, nil, err)
        assert.Equal(t, http.StatusOK, result.StatusCode)

        // case specific expectation
        missing := newResponse("Wrong authorization header", http.StatusUnauthorized)
        expected := ignoreTimestamp(missing.Body)
        actual := ignoreTimestamp(result.Body)
        // inspect content
        assert.Equal(t, expected, actual)
}
```
Calling the Go test runner:
```bash
go test $PWD/cmd/auth
```
should give a `FAIL` with output resembling:
```bash
--- FAIL: TestWrongExtensionSecret (0.00s)
    main_test.go:89:
                Error Trace:    main_test.go:89
                Error:          Not equal:
                                expected: "{\"errors\":[{\"code\":\"DEMO-401\",\"detail\":\
"Wrong authorization header\",,\"status\":\"401\"}]}"
                                actual  : "{\"Location\":\"https://app.pavlok.com/oauth/aut
horize?client_id=\\u0026redirect_uri=\\u0026response_type=code\\u0026state=2e54fb84beedc5b2
53be28d3dac67edf6bae84141b1b2ba56f3668a0e646100e1c156721e0\"}"

                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1 +1 @@
                                -{"errors":[{"code":"DEMO-401","detail":"Wrong authorizat
ion header",,"status":"401"}]}
                                +{"Location":"https://app.pavlok.com/oauth/authorize?client
_id=\u0026redirect_uri=\u0026response_type=code\u0026state=2e54fb84beedc5b253be28d3dac67edf
6bae84141b1b2ba56f3668a0e646100e1c156721e0"}
                Test:           TestWrongExtensionSecret
FAIL
FAIL    github.com/shrmpy/pavlok/cmd/auth       0.015s
FAIL
```
The output tells us that the expected result was a JSON struct containing the `401` status code. Except the actual result was JSON containing the Pavlok OAuth hyperlink; the handler ran *normally*. In the test, we purposely wire-up incorrect values for the `Authorization` header to force the error scenario. So this indicates that the current logic ignores the header contents.

## &sect; Refactor
To trigger the verification of the extension secret, we add the [claims-extraction call](https://gist.github.com/shrmpy/4017db7f99d1cb9d35f01f68bfed39a9)  to the bottom of the `preprocess` [function](https://github.com/shrmpy/pavlok/blob/71d9db338da142d436707a590b8fc738b44be542/cmd/auth/preprocess.go):
```go
        // extension secret is enforced by claims extraction
        cl, err := helper.claims(token)
        if err != nil {
                log.Print("Malformed claims meta data")
                return newResponse("Wrong authorization header", http.StatusUnauthorized),
errors.New("claims")
        }

        log.Printf("Claims (ch/role): %s / %s", cl.ChannelID, cl.Role)

        return events.APIGatewayProxyResponse{}, nil
}
```

Run the test again:
```bash
go test $PWD/cmd/auth
```
which should be clean and free of `FAIL` test output.

Then make sure we follow the Go coding standards:
```bash
go fmt $PWD/cmd/auth
```
Now we can commit the change:
```bash
git add $PWD/cmd/auth
git commit -m'add claims extraction in preprocess'
git push origin gh-issue-num
```
Then navigate to the git repo, and create the merge request. This should launch the deploy preview on Netlify.

Once the preview logs look normal, it's safe to complete the merge request back at the git repo, and remove the patch branch. Netlify should launch a new build for production based on the `main` branch.

After the build is done, visit the Twitch extensions console to check that the panel is still okay.
- The `shock` button will create entries in the Netlify function logs
    ![auth func log](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0av2xag059u6bt5uv0jw.png) ![shock func log](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3lvkt840lgx480dzm0f2.png)
- The API response will be added in Discord via the webhook
    ![discord chan](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wosh9y2unbt7alaae0x5.png)

This was enough to get the test to work, and the desired behavior. Refactoring can easily snowball. So we try to chip away, and be careful to *minimize* taking two steps backwards. 
____
### &#42; Notes, Lessons, Monologue
*Why is a refactor needed?* The code is developed in iterations. In the early rounds, it's a race to get behavior to work. To achieve this, it is normal to follow the "happy path" and postpone negative cases for later. Due to the API requirements, significant work was already interleaved into the iterations to be able to exercise the API calls. So the refactor should be more manageable and not as daunting.

*WTF, you made a tutorial that deployed unfinished code?!* True. The series is my journey into Twitch extension development. Whenever there is a trade-off, I try to "do no harm". So now, it's valuable to analyze the authorization in public view. To get more eyes, and reveal any gaps.

*Why write tests?* In refactoring code, it is best to put tests in place before any changes. The tests should document expected behavior. Then in the process of changes, run the tests frequently to catch breaking code as early as possible.

*Why are there no mocks?* Unfortunately, it's laziness. More refactoring is necessary to make the package testable. Probably lump it with the middleware tech debt.

*Why no tests for positive cases?* For now, relying on old fashioned manual interaction to verify. More refactoring is required before automation is friction-less (e.g., configure environment variables, mocks).

*Why verify the header, things seem to work fine?* The `Authorization` header tells the EBS that a request originates from a trusted application. If we allow all requests, the EBS will be flooded and resource limits for the free tier will be exhausted.