├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .goreleaser.yml
├── LICENSE
├── README.md
├── go.mod
├── go.sum
├── image
├── osbt.png
└── usecase.png
├── main.go
├── oidc
├── op
│ └── attacker
│ │ └── main.go
└── rp
│ └── user-selected
│ └── main.go
├── proxy
└── extension.py
└── server
└── server.go
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | jobs:
9 | goreleaser:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v2
14 | with:
15 | fetch-depth: 0 # important for GoReleaser to access the full history
16 |
17 | - name: Set up Go
18 | uses: actions/setup-go@v2
19 | with:
20 | go-version: 1.18
21 |
22 | - name: Run GoReleaser
23 | uses: goreleaser/goreleaser-action@v2
24 | with:
25 | version: latest
26 | args: release --rm-dist
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 |
30 | - name: Get release
31 | id: get_release
32 | run: |
33 | upload_url=$(curl --silent "https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ github.ref_name }}" \
34 | -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" | jq -r .upload_url)
35 | echo "::set-output name=upload_url::$upload_url"
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 |
39 | - name: Upload Release Asset
40 | id: upload-release-asset
41 | uses: actions/upload-release-asset@v1
42 | env:
43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 | with:
45 | upload_url: ${{ steps.get_release.outputs.upload_url }}
46 | asset_path: ./proxy/extension.py
47 | asset_name: proxy-extension.py
48 | asset_content_type: application/octet-stream
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # Build customization
2 | builds:
3 | - id: osbt
4 | main: ./main.go
5 | binary: osbt
6 | goos:
7 | - linux
8 | - windows
9 | - darwin
10 | goarch:
11 | - amd64
12 | - id: attacker-op
13 | main: ./oidc/op/attacker/main.go
14 | binary: attacker-op
15 | goos:
16 | - linux
17 | - windows
18 | - darwin
19 | goarch:
20 | - amd64
21 |
22 | # Archive customization
23 | archives:
24 | - id: osbt
25 | builds:
26 | - osbt
27 | format: binary
28 | - id: attacker-op
29 | builds:
30 | - attacker-op
31 | format: binary
32 |
33 | # Release customization
34 | release:
35 | github:
36 | owner: oidc-scenario-based-tester
37 | name: osbt
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 oidc-scenario-based-tester
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # osbt
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | OIDC Scenario Based Tester (OSBT) is a testing tool designed to allow the flexible creation of OAuth 2.0 and OpenID Connect test scenarios using Python.
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | OSBT serves to execute more complex and realistic test scenarios against libraries and applications based on OAuth2.0 and OpenID Connect. Testers can construct and execute test scenarios by programming operations such as browser automation, manipulation of proxy servers, and actions of a malicious OpenID provider using the scenario description library provided by OSBT. It also supports integration into CI using GitHub Actions, which can be used for continuous automated security assessment of library applications, and reports showing test results are automatically posted to Issues.
26 |
27 | ## Demo
28 | [DEMO Movie](https://drive.google.com/file/d/1D8Mq26oQTBgfmq8JXPsA5rAnySvaLAqD/view?usp=drive_link)
29 | - A comparison of demonstrations for the detection of the following vulnerabilities through manual testing and OSBT.
30 | - redirect_uri bypass via Auto Biding ([details](https://github.com/oidc-scenario-based-tester/detection-demo))
31 | - redirect_uri session poisoning ([details](https://github.com/oidc-scenario-based-tester/detection-demo))
32 | - Demonstrations for using OSBT on GitHub Actions.
33 |
34 |
35 | ## Features
36 | - **Easy to customize**: test by scripting scenarios in Python.
37 | - **Free manipulation of HTTP traces**: interact with [mitmproxy](https://mitmproxy.org/) extension to freely manipulate HTTP traces.
38 | - **More realistic scenario**: testing with malicious OpenID Provider(OP).
39 | - **Useful CLI tool**: Automatic report generation from test execution results.
40 |
41 | ## Install
42 | ### CLI tool
43 | Download the binary from the Releases page, or compile it from the source.
44 | #### Linux (amd64)
45 | ```
46 | $ curl -Lo osbt https://github.com/oidc-scenario-based-tester/osbt/releases/download/v0.0.2/osbt_0.0.2_linux_amd64
47 | $ sudo mv osbt /usr/local/bin/
48 | $ sudo chmod +x /usr/local/bin/osbt
49 | ```
50 | ### Attcker OP
51 | Download the binary from the Releases page, or compile it from the source.
52 | #### Linux (amd64)
53 | ```
54 | $ curl -Lo attacker-op https://github.com/oidc-scenario-based-tester/osbt/releases/download/v0.0.2/attacker-op_0.0.2_linux_amd64
55 | $ sudo mv attacker-op /usr/local/bin/
56 | $ sudo chmod +x /usr/local/bin/attacker-op
57 | ```
58 | ### Proxy Extension
59 | Download the source from the Releases page.
60 | ```
61 | $ curl -Lo proxy-extension.py https://github.com/oidc-scenario-based-tester/osbt/releases/download/v0.0.2/proxy-extension.py
62 | ```
63 |
64 | ## Usage
65 | 1. Create a scenario
66 | You need to write a scenario to run a test using this tool.
67 |
68 | Please refer to [oidc-scenario-based-tester/osbtlib](https://github.com/oidc-scenario-based-tester/osbtlib) for how to write scenarios.
69 |
70 | 2. Start osbt server
71 |
72 | Please check [Server](#server).
73 |
74 | 3. Start attacker OP
75 |
76 | Please check [Attacker OP](#attacker-op).
77 |
78 | 4. Start proxy extension
79 |
80 | Please check [Proxy Extension](#proxy-extension).
81 |
82 | 5. Run tests
83 |
84 | Please check [Run](#run).
85 |
86 | 6. Get result
87 |
88 | Please check [Result](#result).
89 |
90 | 7. Get a report
91 |
92 | Please check [Report](#report).
93 |
94 | ### Server
95 | You must start the server to collect test results and generate a report.
96 | ```
97 | $ osbt server
98 | ```
99 |
100 | By default, the HTTP server runs at port `54454` on localhost.
101 |
102 | This server interacts with scenarios, collects test results, and handles result retrieval and report generation.
103 |
104 | ### Attacker OP
105 | You must start the attacker OP to run tests that assume a malicious OP.
106 |
107 | ```
108 | $ attacker-op
109 | ```
110 | By default, the attacker OP runs at port `9997` on localhost.
111 |
112 | Attacker OP behaves as a malicious OP that does not follow the protocol specification. It supports the following behaviors that do not conform to the protocol specification.
113 | - ID Token Replacement for Responses
114 | - Providing malicious endpoints using the Discovery service
115 | - Redirect to Honest OP upon an authentication request
116 |
117 | The functionality of attacker OP will be expanded in the future.
118 |
119 | ### Proxy Extension
120 | You must start the proxy extension to manipulate HTTP traces(request/response) between the browser and RP/OP.
121 |
122 | ```
123 | $ mitmdump --ssl-insecure -s proxy-extension.py
124 | ```
125 |
126 | By default, mitmproxy runs at port `8080` and the extension server runs at port `5555` on localhost.
127 |
128 | This extension supports the following HTTP trace operations.
129 | - Adding or tampering with request headers
130 | - Adding or tampering with request query params
131 | - Adding or tampering with request body params
132 | - Interception of requests and responses based on conditions
133 | - Obtaining request and response histories
134 |
135 | The functionality of proxy extension will be expanded in the future.
136 |
137 | ### Run
138 | After the setup is complete, you can run test scenarios.
139 |
140 | ```
141 | $ osbt run -f scenario.py -t 30
142 | ```
143 | > #### options
144 | >
145 | > - `-f`, `--file` scenario file (required)
146 | > - `-d`, `--dir` test directory to run all tests
147 | > - `-r`, `--recursive` search directories recursively
148 | > - `-t`, `--timeout` scenario execution timeout (default 30s)
149 |
150 | ### Result
151 | You can get the result by sending a request to the server's `/results` endpoints after testing.
152 |
153 | ```
154 | $ curl http://localhost:54454/results | jq
155 | [
156 | {
157 | "test_name": "IDSpoofing",
158 | "description": "\n- The attacker op modifies the id_token to impersonate the victim
- The sub claim of the id_token is modified to the victim's sub claim\n",
159 | "outcome": "Passed",
160 | "err_msg": "",
161 | "countermeasure": "\n- Check the signature of the id_token
- Check the iss claim of the id_token
- Check the sub claim of the id_token\n"
162 | },
163 | ...
164 | ```
165 |
166 | ### Report
167 | You can generate a report by sending a request to the server's `/report` endpoints after testing.
168 |
169 | ```
170 | $ curl http://localhost:54454/report
171 | # Test Results
172 |
173 | Tests conducted:
174 | - IDSpoofing
175 |
176 | ## IDSpoofing
177 |
178 | | | |
179 | | --- | --- |
180 | | Description |
181 | - The attacker op modifies the id_token to impersonate the victim
- The sub claim of the id_token is modified to the victim's sub claim
182 | |
183 | | Outcome | Passed |
184 | | Error Message | |
185 | | Countermeasure |
186 | - Check the signature of the id_token
- Check the iss claim of the id_token
- Check the sub claim of the id_token
187 | |
188 |
189 | Report generated by OSBT.
190 | ```
191 |
192 | ## CI Integration
193 | You can also use osbt in github actions.
194 |
195 | Please refer to the [actions.yml](https://github.com/oidc-scenario-based-tester/actions-demo/blob/main/.github/workflows/actions.yml) in the [oidc-scenario-based-tester/actions-demo](https://github.com/oidc-scenario-based-tester/actions-demo/).
196 |
197 | 1. Put test scenarios in `/test` directory.
198 |
199 | https://github.com/oidc-scenario-based-tester/actions-demo/tree/main/test
200 |
201 | 2. Setup target OP and RP.
202 | ```yaml
203 | - name: setup rp
204 | run: >
205 | CLIENT_ID=web
206 | CLIENT_SECRET=secret
207 | ISSUER=http://localhost:9998/
208 | SCOPES="openid profile"
209 | PORT=9999
210 | go run github.com/oidc-scenario-based-tester/actions-demo/oidc/rp &
211 | shell: bash
212 |
213 | - name: setup op
214 | run: |
215 | go run github.com/oidc-scenario-based-tester/actions-demo/oidc/op &
216 | shell: bash
217 | ```
218 |
219 | 3. Setup osbt and run tests.
220 |
221 | You can use [osbt-setup-actions](https://github.com/oidc-scenario-based-tester/osbt-setup-actions).
222 | ```yaml
223 | - name: osbt setup
224 | uses: oidc-scenario-based-tester/osbt-setup-actions@v0.0.1
225 | with:
226 | version: '0.0.1'
227 | ...
228 | - name: run test
229 | run: |
230 | osbt run -d ./test
231 | shell: bash
232 | ```
233 |
234 | 4. Create a test report on issue.
235 |
236 | You can use [osbt-report-actions](https://github.com/oidc-scenario-based-tester/osbt-report-actions).
237 |
238 | ```yaml
239 | - name: osbt report
240 | uses: oidc-scenario-based-tester/osbt-report-actions@v0.0.1
241 | ```
242 | ---
243 |
244 | image: [Flaticon.com](https://www.flaticon.com/)
245 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/oidc-scenario-based-tester/osbt
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/bytedance/sonic v1.9.1 // indirect
7 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
8 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
9 | github.com/gin-contrib/sse v0.1.0 // indirect
10 | github.com/gin-gonic/gin v1.9.1 // indirect
11 | github.com/go-playground/locales v0.14.1 // indirect
12 | github.com/go-playground/universal-translator v0.18.1 // indirect
13 | github.com/go-playground/validator/v10 v10.14.0 // indirect
14 | github.com/goccy/go-json v0.10.2 // indirect
15 | github.com/golang/protobuf v1.5.3 // indirect
16 | github.com/google/uuid v1.3.0 // indirect
17 | github.com/gorilla/mux v1.8.0 // indirect
18 | github.com/gorilla/schema v1.2.0 // indirect
19 | github.com/gorilla/securecookie v1.1.1 // indirect
20 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
21 | github.com/json-iterator/go v1.1.12 // indirect
22 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect
23 | github.com/leodido/go-urn v1.2.4 // indirect
24 | github.com/mattn/go-isatty v0.0.19 // indirect
25 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
26 | github.com/modern-go/reflect2 v1.0.2 // indirect
27 | github.com/muhlemmer/gu v0.3.1 // indirect
28 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect
29 | github.com/rs/cors v1.9.0 // indirect
30 | github.com/sirupsen/logrus v1.9.3 // indirect
31 | github.com/spf13/cobra v1.7.0 // indirect
32 | github.com/spf13/pflag v1.0.5 // indirect
33 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
34 | github.com/ugorji/go/codec v1.2.11 // indirect
35 | github.com/zitadel/oidc/v2 v2.7.0 // indirect
36 | golang.org/x/arch v0.3.0 // indirect
37 | golang.org/x/crypto v0.11.0 // indirect
38 | golang.org/x/net v0.12.0 // indirect
39 | golang.org/x/oauth2 v0.10.0 // indirect
40 | golang.org/x/sys v0.10.0 // indirect
41 | golang.org/x/text v0.11.0 // indirect
42 | google.golang.org/appengine v1.6.7 // indirect
43 | google.golang.org/protobuf v1.31.0 // indirect
44 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect
45 | gopkg.in/yaml.v3 v3.0.1 // indirect
46 | )
47 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
2 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
3 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
4 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
5 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
7 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
11 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
12 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
13 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
14 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
15 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
16 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
17 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
18 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
19 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
20 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
21 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
22 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
23 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
24 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
25 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
26 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
27 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
28 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
29 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
30 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
31 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
32 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
33 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
34 | github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
35 | github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
36 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
37 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
38 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
39 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
40 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
41 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
42 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
43 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
44 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
45 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
46 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
47 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
48 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
49 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
52 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
53 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
54 | github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
55 | github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
56 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
57 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
58 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
59 | github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
60 | github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
61 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
62 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
63 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
64 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
65 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
66 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
67 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
68 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
69 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
70 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
71 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
72 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
73 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
74 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
75 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
76 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
77 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
78 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
79 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
80 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
81 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
82 | github.com/zitadel/oidc/v2 v2.7.0 h1:IGX4EDk6tegTjUSsZDWeTfLseFU0BdJ/Glf1tgys2lU=
83 | github.com/zitadel/oidc/v2 v2.7.0/go.mod h1:zkUkVJS0sDVy9m0UA9RgO3f8i/C0rtjvXU36UJj7T+0=
84 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
85 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
86 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
87 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
88 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
89 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
90 | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
91 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
92 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
93 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
94 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
95 | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
96 | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
97 | golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
98 | golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
99 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
100 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
102 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
103 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
104 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
105 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
106 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
107 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
108 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
109 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
110 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
111 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
112 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
113 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
114 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
115 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
116 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
117 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
118 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
119 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
120 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
121 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
122 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
123 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
124 | gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
125 | gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
126 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
127 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
128 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
129 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
130 |
--------------------------------------------------------------------------------
/image/osbt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oidc-scenario-based-tester/osbt/9354588853a9924bf9e661251e1a9d77cfd356fa/image/osbt.png
--------------------------------------------------------------------------------
/image/usecase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oidc-scenario-based-tester/osbt/9354588853a9924bf9e661251e1a9d77cfd356fa/image/usecase.png
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "os"
8 | "os/exec"
9 | "path/filepath"
10 | "time"
11 |
12 | "github.com/oidc-scenario-based-tester/osbt/server"
13 | "github.com/spf13/cobra"
14 | )
15 |
16 | func main() {
17 | var rootCmd = &cobra.Command{Use: "osbt"}
18 | var runCmd = &cobra.Command{
19 | Use: "run",
20 | Short: "Run the tests",
21 | Run: runTests,
22 | }
23 | var serverCmd = &cobra.Command{
24 | Use: "server",
25 | Short: "Start the server that receives and saves the test results",
26 | Run: func(cmd *cobra.Command, args []string) {
27 | server.StartServer()
28 | },
29 | }
30 |
31 | runCmd.Flags().StringP("file", "f", "", "Specify the test file to run")
32 | runCmd.Flags().StringP("dir", "d", "", "Specify the test directory to run all tests")
33 | runCmd.Flags().BoolP("recursive", "r", false, "Search directories recursively")
34 | runCmd.Flags().StringP("timeout", "t", "30s", "Specify the timeout for running tests")
35 |
36 | rootCmd.AddCommand(runCmd, serverCmd)
37 | rootCmd.Execute()
38 | }
39 |
40 | func runTests(cmd *cobra.Command, args []string) {
41 | file, _ := cmd.Flags().GetString("file")
42 | dir, _ := cmd.Flags().GetString("dir")
43 | recursive, _ := cmd.Flags().GetBool("recursive")
44 | timeout, _ := cmd.Flags().GetString("timeout")
45 |
46 | timeoutDuration, err := time.ParseDuration(timeout)
47 | if err != nil {
48 | fmt.Printf("Invalid timeout value: %v", err)
49 | os.Exit(1)
50 | }
51 |
52 | if file != "" {
53 | runTest(file, timeoutDuration)
54 | }
55 |
56 | if dir != "" {
57 | runTestsInDir(dir, recursive, timeoutDuration)
58 | }
59 | }
60 |
61 | func runTest(file string, timeout time.Duration) {
62 | ctx, cancel := context.WithTimeout(context.Background(), timeout)
63 | defer cancel()
64 |
65 | cmd := exec.CommandContext(ctx, "python", file)
66 | var out bytes.Buffer
67 | cmd.Stdout = &out
68 | err := cmd.Run()
69 | if err != nil {
70 | if ctx.Err() == context.DeadlineExceeded {
71 | fmt.Printf("Timeout running test: %s\n", file)
72 | } else {
73 | fmt.Printf("Error running test: %v\n", err)
74 | }
75 | } else {
76 | fmt.Printf("Test result: %s\n", out.String())
77 | }
78 | }
79 |
80 | func runTestsInDir(dir string, recursive bool, timeout time.Duration) {
81 | err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
82 | if err != nil {
83 | return err
84 | }
85 | if !info.IsDir() && filepath.Ext(path) == ".py" {
86 | runTest(path, timeout)
87 | }
88 | return nil
89 | })
90 | if err != nil {
91 | fmt.Printf("Error running tests in directory: %v\n", err)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/oidc/op/attacker/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "log"
9 | "net/http"
10 | "net/url"
11 | "sync"
12 |
13 | "github.com/google/uuid"
14 | "github.com/gorilla/mux"
15 | "github.com/zitadel/oidc/v2/example/server/exampleop"
16 | "github.com/zitadel/oidc/v2/example/server/storage"
17 | )
18 |
19 | type Task struct {
20 | Name string
21 | Args map[string]string
22 | }
23 |
24 | var tasks = make(map[string]Task)
25 | var lock sync.RWMutex
26 |
27 | func main() {
28 | port := "9997"
29 | issuer := fmt.Sprintf("http://localhost:%s/", port)
30 |
31 | userStore := storage.NewUserStore(issuer)
32 | storage := storage.NewStorage(userStore)
33 |
34 | opHandler := exampleop.SetupServer(issuer, storage)
35 |
36 | router := mux.NewRouter()
37 | // Add the task routes.
38 | router.HandleFunc("/task/{id}", getTask).Methods("GET")
39 | router.HandleFunc("/task", addTask).Methods("POST")
40 | router.HandleFunc("/task", deleteTasks).Methods("DELETE")
41 |
42 | router.PathPrefix("/").Handler(opHandler)
43 |
44 | // Add the task middleware.
45 | router.Use(requestMiddleware, responseMiddleware)
46 |
47 | server := &http.Server{
48 | Addr: "localhost:" + port,
49 | Handler: router,
50 | }
51 | log.Printf("server listening on http://localhost:%s/", port)
52 | log.Println("press ctrl+c to stop")
53 | err := server.ListenAndServe()
54 | if err != nil {
55 | log.Fatal(err)
56 | }
57 | }
58 |
59 | func getTask(w http.ResponseWriter, r *http.Request) {
60 | vars := mux.Vars(r)
61 | id := vars["id"]
62 |
63 | lock.RLock()
64 | defer lock.RUnlock()
65 |
66 | task, exists := tasks[id]
67 | if exists {
68 | json.NewEncoder(w).Encode(task)
69 | } else {
70 | http.Error(w, "Task not found", http.StatusNotFound)
71 | }
72 | }
73 |
74 | func addTask(w http.ResponseWriter, r *http.Request) {
75 | var t Task
76 | if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
77 | http.Error(w, err.Error(), http.StatusBadRequest)
78 | return
79 | }
80 | log.Printf("task name: %s", t.Name)
81 | log.Printf("task args: %v", t.Args)
82 |
83 | lock.Lock()
84 | defer lock.Unlock()
85 |
86 | taskID := uuid.New().String()
87 | task := Task{
88 | Name: t.Name,
89 | Args: t.Args,
90 | }
91 | tasks[taskID] = task
92 |
93 | json.NewEncoder(w).Encode(map[string]string{"taskId": taskID})
94 | }
95 |
96 | func deleteTasks(w http.ResponseWriter, r *http.Request) {
97 | lock.Lock()
98 | defer lock.Unlock()
99 |
100 | tasks = make(map[string]Task)
101 |
102 | json.NewEncoder(w).Encode(map[string]string{"message": "Tasks deleted"})
103 | }
104 |
105 | func requestMiddleware(next http.Handler) http.Handler {
106 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
107 | var bodyBytes []byte
108 | if r.Body != nil {
109 | bodyBytes, _ = ioutil.ReadAll(r.Body)
110 | }
111 |
112 | // Restore the io.ReadCloser to its original state
113 | r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
114 |
115 | // Use the content
116 | bodyString := string(bodyBytes)
117 | log.Printf("Request Body: %s", bodyString)
118 |
119 | log.Printf("Before middleware: Method - %s, Path - %s, Query Params - %v, Request Body - %s\n", r.Method, r.URL.Path, r.URL.Query(), bodyString)
120 |
121 | for _, task := range tasks {
122 | log.Printf("task name: %s", task.Name)
123 | log.Printf("task args: %v", task.Args)
124 |
125 | if task.Name == "IdPConfusion" {
126 | if r.Method == "GET" && r.URL.Path == "/auth" {
127 | log.Printf("IdPConfusion task")
128 | if honestIdpAuthEndpoint, ok := task.Args["honest_idp_auth_endpoint"]; ok {
129 | parsedUrl, err := url.Parse(honestIdpAuthEndpoint)
130 | if err != nil {
131 | log.Printf("Could not parse URL: %v", err)
132 | return
133 | }
134 | parsedUrl.RawQuery = r.URL.RawQuery
135 | http.Redirect(w, r, parsedUrl.String(), http.StatusFound)
136 | return // early return after redirect
137 | }
138 | }
139 | }
140 | }
141 |
142 | log.Printf("After middleware: Method - %s, Path - %s, Query Params - %v, Request Body - %s\n", r.Method, r.URL.Path, r.URL.Query(), bodyString)
143 |
144 | next.ServeHTTP(w, r)
145 | })
146 | }
147 |
148 | func responseMiddleware(next http.Handler) http.Handler {
149 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
150 | rw := NewResponseWriterInterceptor(w)
151 | next.ServeHTTP(rw, r)
152 |
153 | // Print out method, path, and body before middleware operations
154 | var body []byte
155 | if r.Body != nil {
156 | body, _ = ioutil.ReadAll(r.Body)
157 | }
158 | log.Printf("Before middleware: Method - %s, Path - %s, Query Params - %v, Request Body - %s, Response Body - %s\n", r.Method, r.URL.Path, r.URL.Query(), string(body), rw.Body)
159 |
160 | // assuming we always want to handle the first added task
161 | for _, task := range tasks {
162 | log.Printf("task name: %s", task.Name)
163 | log.Printf("task args: %v", task.Args)
164 | if task.Name == "IDSpoofing" {
165 | if r.Method == "POST" && r.URL.Path == "/oauth/token" {
166 | log.Printf("IDSpoofing task")
167 | // Parse the JSON body
168 | var body map[string]interface{}
169 | if err := json.Unmarshal(rw.Body, &body); err != nil {
170 | log.Printf("Could not parse JSON body: %v", err)
171 | return
172 | }
173 |
174 | // Replace the ID token
175 | if idToken, ok := task.Args["id_token"]; ok {
176 | body["id_token"] = idToken
177 | newBody, err := json.Marshal(body)
178 | if err != nil {
179 | log.Printf("Could not marshal JSON body: %v", err)
180 | return
181 | }
182 |
183 | // Replace the body in the ResponseWriterInterceptor
184 | rw.Body = newBody
185 | }
186 | }
187 | }
188 |
189 | if task.Name == "MaliciousEndpoint" {
190 | if r.Method == "GET" && r.URL.Path == "/.well-known/openid-configuration" {
191 | log.Printf("MaliciousEndpoint task")
192 | // Parse the JSON body
193 | var body map[string]interface{}
194 | if err := json.Unmarshal(rw.Body, &body); err != nil {
195 | log.Printf("Could not parse JSON body: %v", err)
196 | return
197 | }
198 |
199 | keys := []string{"issuer", "authorization_endpoint", "token_endpoint", "userinfo_endpoint", "registration_endpoint"}
200 | for key, value := range task.Args {
201 | for _, k := range keys {
202 | if key == k {
203 | body[key] = value
204 | break
205 | }
206 | }
207 | }
208 |
209 | newBody, err := json.Marshal(body)
210 | if err != nil {
211 | log.Printf("Could not marshal JSON body: %v", err)
212 | return
213 | }
214 |
215 | // Replace the body in the ResponseWriterInterceptor
216 | rw.Body = newBody
217 | }
218 | }
219 | }
220 | // Print out method, path, and body after middleware operations
221 | log.Printf("After middleware: Method - %s, Path - %s, Query Params - %v, Request Body - %s, Response Body - %s\n", r.Method, r.URL.Path, r.URL.Query(), string(body), rw.Body)
222 | rw.WriteToResponse()
223 | })
224 | }
225 |
226 | type ResponseWriterInterceptor struct {
227 | http.ResponseWriter
228 | Body []byte
229 | wroteHeader bool
230 | }
231 |
232 | func NewResponseWriterInterceptor(w http.ResponseWriter) *ResponseWriterInterceptor {
233 | return &ResponseWriterInterceptor{
234 | ResponseWriter: w,
235 | Body: []byte{},
236 | }
237 | }
238 |
239 | func (rw *ResponseWriterInterceptor) Write(b []byte) (int, error) {
240 | rw.Body = append(rw.Body, b...)
241 | return len(b), nil
242 | }
243 |
244 | func (rw *ResponseWriterInterceptor) WriteToResponse() {
245 | rw.ResponseWriter.Write(rw.Body)
246 | }
247 |
--------------------------------------------------------------------------------
/oidc/rp/user-selected/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "os"
9 | "strings"
10 | "time"
11 |
12 | "github.com/google/uuid"
13 | "github.com/sirupsen/logrus"
14 |
15 | "github.com/zitadel/oidc/v2/pkg/client/rp"
16 | httphelper "github.com/zitadel/oidc/v2/pkg/http"
17 | "github.com/zitadel/oidc/v2/pkg/oidc"
18 | )
19 |
20 | var (
21 | callbackPath = "/auth/callback"
22 | key = []byte("test1234test1234")
23 | rps = make(map[string]*rp.RelyingParty)
24 | )
25 |
26 | func generateStateWithIssuer(issuer string) func() string {
27 | return func() string {
28 | state := uuid.New().String()
29 | return state + ":" + issuer
30 | }
31 | }
32 |
33 | func getRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...rp.Option) (*rp.RelyingParty, error) {
34 | // if rp, ok := rps[issuer]; ok { // if instance already exists, return it
35 | // return rp, nil
36 | // }
37 |
38 | rp, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) // otherwise create a new instance
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | // rps[issuer] = &rp // save the instance in the map
44 | return &rp, nil
45 | }
46 |
47 | func main() {
48 | clientID := os.Getenv("CLIENT_ID")
49 | clientSecret := os.Getenv("CLIENT_SECRET")
50 | keyPath := os.Getenv("KEY_PATH")
51 | port := os.Getenv("PORT")
52 | scopes := strings.Split(os.Getenv("SCOPES"), " ")
53 |
54 | redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
55 | cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
56 |
57 | options := []rp.Option{
58 | rp.WithCookieHandler(cookieHandler),
59 | rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
60 | }
61 | if clientSecret == "" {
62 | options = append(options, rp.WithPKCE(cookieHandler))
63 | }
64 | if keyPath != "" {
65 | options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
66 | }
67 |
68 | // provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...)
69 | // if err != nil {
70 | // logrus.Fatalf("error creating provider %s", err.Error())
71 | // }
72 |
73 | // generate some state (representing the state of the user in your application,
74 | // e.g. the page where he was before sending him to login
75 | // state := func() string {
76 | // return uuid.New().String()
77 | // }
78 |
79 | // register the AuthURLHandler at your preferred path.
80 | // the AuthURLHandler creates the auth request and redirects the user to the auth server.
81 | // including state handling with secure cookie and the possibility to use PKCE.
82 | // Prompts can optionally be set to inform the server of
83 | // any messages that need to be prompted back to the user.
84 | http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
85 | issuer := r.URL.Query().Get("issuer")
86 | log.Printf("issuer: %s", issuer)
87 | if issuer == "" {
88 | http.Error(w, "Issuer not provided", http.StatusBadRequest)
89 | return
90 | }
91 |
92 | provider, err := getRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...)
93 | if err != nil {
94 | http.Error(w, err.Error(), http.StatusInternalServerError)
95 | return
96 | }
97 | rp.AuthURLHandler(generateStateWithIssuer(issuer), *provider, rp.WithPromptURLParam("Welcome back!")).ServeHTTP(w, r)
98 | })
99 |
100 | // for demonstration purposes the returned userinfo response is written as JSON object onto response
101 | marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
102 | data, err := json.Marshal(info)
103 | if err != nil {
104 | http.Error(w, err.Error(), http.StatusInternalServerError)
105 | return
106 | }
107 | w.Write(data)
108 | }
109 |
110 | // you could also just take the access_token and id_token without calling the userinfo endpoint:
111 | //
112 | // marshalToken := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) {
113 | // data, err := json.Marshal(tokens)
114 | // if err != nil {
115 | // http.Error(w, err.Error(), http.StatusInternalServerError)
116 | // return
117 | // }
118 | // w.Write(data)
119 | //}
120 |
121 | // you can also try token exchange flow
122 | //
123 | // requestTokenExchange := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
124 | // data := make(url.Values)
125 | // data.Set("grant_type", string(oidc.GrantTypeTokenExchange))
126 | // data.Set("requested_token_type", string(oidc.IDTokenType))
127 | // data.Set("subject_token", tokens.RefreshToken)
128 | // data.Set("subject_token_type", string(oidc.RefreshTokenType))
129 | // data.Add("scope", "profile custom_scope:impersonate:id2")
130 |
131 | // client := &http.Client{}
132 | // r2, _ := http.NewRequest(http.MethodPost, issuer+"/oauth/token", strings.NewReader(data.Encode()))
133 | // // r2.Header.Add("Authorization", "Basic "+"d2ViOnNlY3JldA==")
134 | // r2.Header.Add("Content-Type", "application/x-www-form-urlencoded")
135 | // r2.SetBasicAuth("web", "secret")
136 |
137 | // resp, _ := client.Do(r2)
138 | // fmt.Println(resp.Status)
139 |
140 | // b, _ := io.ReadAll(resp.Body)
141 | // resp.Body.Close()
142 |
143 | // w.Write(b)
144 | // }
145 |
146 | // register the CodeExchangeHandler at the callbackPath
147 | // the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
148 | // with the returned tokens from the token endpoint
149 | // in this example the callback function itself is wrapped by the UserinfoCallback which
150 | // will call the Userinfo endpoint, check the sub and pass the info into the callback function
151 | http.HandleFunc(callbackPath, func(w http.ResponseWriter, r *http.Request) {
152 | stateParam := r.URL.Query().Get("state")
153 | log.Printf("state: %s", stateParam)
154 | if stateParam == "" {
155 | http.Error(w, "State not provided", http.StatusBadRequest)
156 | return
157 | }
158 |
159 | firstIndex := strings.Index(stateParam, ":")
160 | if firstIndex == -1 || firstIndex == len(stateParam)-1 {
161 | http.Error(w, "Invalid state", http.StatusBadRequest)
162 | return
163 | }
164 | issuer := stateParam[firstIndex+1:]
165 |
166 | provider, err := getRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...)
167 | if err != nil {
168 | http.Error(w, err.Error(), http.StatusInternalServerError)
169 | return
170 | }
171 | rp.CodeExchangeHandler(rp.UserinfoCallback(marshalUserinfo), *provider).ServeHTTP(w, r)
172 | })
173 |
174 | // if you would use the callback without calling the userinfo endpoint, simply switch the callback handler for:
175 | //
176 | // http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider))
177 |
178 | lis := fmt.Sprintf("127.0.0.1:%s", port)
179 | logrus.Infof("listening on http://%s/", lis)
180 | logrus.Info("press ctrl+c to stop")
181 | logrus.Fatal(http.ListenAndServe(lis, nil))
182 | }
183 |
--------------------------------------------------------------------------------
/proxy/extension.py:
--------------------------------------------------------------------------------
1 | from mitmproxy import http, ctx
2 | from http.server import BaseHTTPRequestHandler, HTTPServer
3 | import socket
4 | import threading
5 | import json
6 | import logging
7 | from urllib.parse import urlparse
8 |
9 | class HttpHandler(BaseHTTPRequestHandler):
10 | def do_POST(self):
11 | content_length = int(self.headers['Content-Length'])
12 | post_data = self.rfile.read(content_length)
13 | response = self.server.instance.handle_request(post_data)
14 | self.send_response(200)
15 | self.send_header('Content-type', 'application/json')
16 | self.end_headers()
17 | self.wfile.write(json.dumps(response).encode())
18 |
19 | def log_message(self, format, *args):
20 | return
21 |
22 | class HTTPServer(HTTPServer):
23 | def __init__(self, server_address, handler_class, instance):
24 | self.instance = instance
25 | super().__init__(server_address, handler_class)
26 |
27 | class MITMInterceptor:
28 | def __init__(self):
29 | self.added_header = []
30 | self.modified_header = []
31 | self.added_query_param = []
32 | self.modified_query_param = []
33 | self.added_body_param = []
34 | self.modified_body_param = []
35 | self.intercepted_request = []
36 | self.intercepted_response = []
37 | self.history_request = []
38 | self.history_response = []
39 |
40 | self.server = HTTPServer(('localhost', 5555), HttpHandler, self)
41 | self.server_thread = threading.Thread(target=self.server.serve_forever)
42 | self.server_thread.start()
43 |
44 | def handle_request(self, data):
45 | json_data = data.decode("utf-8")
46 | dict_data = json.loads(json_data)
47 |
48 | operation = dict_data.get("operation")
49 | name = dict_data.get("name")
50 | value = dict_data.get("value")
51 | host = dict_data.get("host")
52 | path = dict_data.get("path")
53 | method = dict_data.get("method")
54 | logging.info(f"operation: {operation}, name: {name}, value: {value}, host: {host}, path: {path}, method: {method}")
55 |
56 | if operation == "add_header":
57 | self.added_header.append((name, value, host, path, method))
58 | return {"status": "ok"}
59 | elif operation == "modify_header":
60 | self.modified_header.append((name, value, host, path, method))
61 | return {"status": "ok"}
62 | elif operation == "add_query_param":
63 | self.added_query_param.append((name, value, host, path, method))
64 | return {"status": "ok"}
65 | elif operation == "modify_query_param":
66 | self.modified_query_param.append((name, value, host, path, method))
67 | return {"status": "ok"}
68 | elif operation == "add_body_param":
69 | self.added_body_param.append((name, value, host, path, method))
70 | return {"status": "ok"}
71 | elif operation == "modify_body_param":
72 | self.modified_body_param.append((name, value, host, path, method))
73 | return {"status": "ok"}
74 | elif operation == "intercept_request":
75 | self.intercepted_request.append((host, path, method))
76 | return {"status": "ok"}
77 | elif operation == "intercept_response":
78 | self.intercepted_response.append((host, path, method))
79 | return {"status": "ok"}
80 | elif operation == "clean":
81 | self.clean()
82 | return {"status": "ok"}
83 | elif operation == "get_history":
84 | return {"status": "ok", "request": self.history_request, "response": self.history_response}
85 | else:
86 | # Unsupported operation
87 | pass
88 |
89 | def request(self, flow: http.HTTPFlow):
90 | try:
91 | self.store_request(flow)
92 |
93 | request = flow.request
94 | parsed_url = urlparse(request.url)
95 |
96 | host_name = parsed_url.netloc
97 | request_path = parsed_url.path
98 |
99 | headers = request.headers
100 | query = request.query
101 |
102 | for name, value, condition_host, condition_path, condition_method in self.added_header:
103 | if (condition_host is None or condition_host == host_name) and \
104 | (condition_path is None or condition_path == request_path) and \
105 | (condition_method is None or condition_method == request.method):
106 | headers[name] = value
107 |
108 | for target, value, condition_host, condition_path, condition_method in self.modified_header:
109 | if (condition_host is None or condition_host == host_name) and \
110 | (condition_path is None or condition_path == request_path) and \
111 | (condition_method is None or condition_method == request.method) and target in headers:
112 | headers[target] = value
113 |
114 | for name, value, condition_host, condition_path, condition_method in self.added_query_param:
115 | if (condition_host is None or condition_host == host_name) and \
116 | (condition_path is None or condition_path == request_path) and \
117 | (condition_method is None or condition_method == request.method):
118 | query[name] = value
119 |
120 | for target, value, condition_host, condition_path, condition_method in self.modified_query_param:
121 | if (condition_host is None or condition_host == host_name) and \
122 | (condition_path is None or condition_path == request_path) and \
123 | (condition_method is None or condition_method == request.method) and target in query:
124 | query[target] = value
125 |
126 | content = request.content.decode()
127 | for name, value, condition_host, condition_path, condition_method in self.added_body_param:
128 | if (condition_host is None or condition_host == host_name) and \
129 | (condition_path is None or condition_path == request_path) and \
130 | (condition_method is None or condition_method == request.method):
131 | content += "&" + name + "=" + value
132 |
133 | for target, value, condition_host, condition_path, condition_method in self.modified_body_param:
134 | if (condition_host is None or condition_host == host_name) and \
135 | (condition_path is None or condition_path == request_path) and \
136 | (condition_method is None or condition_method == request.method):
137 | content = content.replace(target, value)
138 |
139 | request.content = content.encode()
140 | url = request.pretty_url
141 |
142 | for condition_host, condition_path, condition_method in self.intercepted_request:
143 | if (condition_host is None or condition_host == host_name) and \
144 | (condition_path is None or condition_path == request_path) and \
145 | (condition_method is None or condition_method == request.method):
146 | flow.kill()
147 |
148 | except Exception as e:
149 | logging.info(f"Request Error: {e}")
150 |
151 | def response(self, flow: http.HTTPFlow):
152 | try:
153 | self.store_response(flow)
154 |
155 | response = flow.response
156 |
157 | headers = response.headers
158 | content = response.get_text()
159 |
160 | parsed_url = urlparse(flow.request.url)
161 | host_name = parsed_url.netloc
162 | response_path = parsed_url.path
163 |
164 | for condition_host, condition_path, condition_method in self.intercepted_response:
165 | if (condition_host is None or condition_host == host_name) and \
166 | (condition_path is None or condition_path == response_path) and \
167 | (condition_method is None or condition_method == flow.request.method):
168 | flow.kill()
169 |
170 | except Exception as e:
171 | logging.info(f"Response Error: {e}")
172 |
173 | def store_request(self, flow):
174 | request_data = {
175 | "method": flow.request.method,
176 | "url": flow.request.pretty_url,
177 | "headers": dict(flow.request.headers),
178 | "content": flow.request.get_text(),
179 | }
180 |
181 | self.history_request.append(request_data)
182 |
183 | def store_response(self, flow):
184 | response_data = {
185 | "status_code": flow.response.status_code,
186 | "headers": dict(flow.response.headers),
187 | "content": flow.response.get_text(),
188 | }
189 |
190 | self.history_response.append(response_data)
191 |
192 | def clean(self):
193 | self.added_header = []
194 | self.modified_header = []
195 | self.added_query_param = []
196 | self.modified_query_param = []
197 | self.added_body_param = []
198 | self.modified_body_param = []
199 | self.intercepted_request = []
200 | self.intercepted_response = []
201 | self.history_request = []
202 | self.history_response = []
203 |
204 | def done(self):
205 | try:
206 | print("Closing server")
207 | self.server.shutdown()
208 | self.server_thread.join()
209 | except Exception as e:
210 | print("Failed to properly close server: ", e)
211 |
212 | addons = [
213 | MITMInterceptor()
214 | ]
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 |
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | // TestResult represents a test result received from POST /result/add
11 | type TestResult struct {
12 | TestName string `json:"test_name"`
13 | Description string `json:"description"`
14 | Outcome string `json:"outcome"`
15 | ErrMsg string `json:"err_msg"`
16 | Countermeasure string `json:"countermeasure"`
17 | }
18 |
19 | var (
20 | // results stores all test results received
21 | results []TestResult
22 | // mutex is used to ensure that appending to results is thread-safe
23 | mutex = &sync.Mutex{}
24 | )
25 |
26 | func StartServer() {
27 | r := gin.Default()
28 |
29 | r.POST("/result/add", func(c *gin.Context) {
30 | var result TestResult
31 | if err := c.ShouldBindJSON(&result); err != nil {
32 | c.JSON(400, gin.H{"error": err.Error()})
33 | return
34 | }
35 |
36 | // Append the test result to the results slice in a thread-safe manner
37 | mutex.Lock()
38 | results = append(results, result)
39 | mutex.Unlock()
40 |
41 | c.JSON(200, gin.H{"message": "Test result received successfully"})
42 | })
43 |
44 | r.GET("/results", func(c *gin.Context) {
45 | // Return all test results
46 | c.JSON(200, results)
47 | })
48 |
49 | r.GET("/report", func(c *gin.Context) {
50 | // Generate markdown report from all test results
51 | report := "# OSBT Report\n\n"
52 |
53 | passedTests := []TestResult{}
54 | failedTests := []TestResult{}
55 |
56 | for _, result := range results {
57 | if result.Outcome == "pass" {
58 | passedTests = append(passedTests, result)
59 | } else {
60 | failedTests = append(failedTests, result)
61 | }
62 | }
63 |
64 | totalTests := len(results)
65 | passedPercentage := float64(len(passedTests)) / float64(totalTests) * 100
66 | failedPercentage := float64(len(failedTests)) / float64(totalTests) * 100
67 | report += fmt.Sprintf("Total Tests: %d\n\n", totalTests)
68 | report += fmt.Sprintf("Passed Rate: %.2f%%\n\n", passedPercentage)
69 | report += fmt.Sprintf("Failed Rate: %.2f%%\n\n", failedPercentage)
70 |
71 | report += "## \u274C Failed Tests\n"
72 | report += "Tests conducted:\n"
73 |
74 | for _, result := range failedTests {
75 | report += fmt.Sprintf("- %s\n", result.TestName)
76 | }
77 |
78 | report += "\n"
79 |
80 | for _, result := range failedTests {
81 | report += fmt.Sprintf(
82 | "### %s\n\n| | |\n| --- | --- |\n| Description | %s |\n| Outcome | %s |\n| Error Message | %s |\n| Countermeasure | %s |\n\n",
83 | result.TestName,
84 | result.Description,
85 | result.Outcome,
86 | result.ErrMsg,
87 | result.Countermeasure,
88 | )
89 | }
90 |
91 | report += "## \u2705 Passed Tests\n"
92 | report += "Tests conducted:\n"
93 |
94 | for _, result := range passedTests {
95 | report += fmt.Sprintf("- %s\n", result.TestName)
96 | }
97 |
98 | report += "\n"
99 |
100 | for _, result := range passedTests {
101 | report += fmt.Sprintf(
102 | "### %s\n\n| | |\n| --- | --- |\n| Description | %s |\n| Outcome | %s |\n| Error Message | %s |\n| Countermeasure | %s |\n\n",
103 | result.TestName,
104 | result.Description,
105 | result.Outcome,
106 | result.ErrMsg,
107 | result.Countermeasure,
108 | )
109 | }
110 |
111 | report += "\nReport generated by OSBT."
112 |
113 | c.String(200, report)
114 | })
115 |
116 | r.Run(":54454") // listen and serve on specified port
117 | }
118 |
--------------------------------------------------------------------------------