├── .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 | GitHub Workflow Status 5 | 6 | 7 | license 8 | 9 | 10 | release 11 | 12 | 13 | downloads 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 | --------------------------------------------------------------------------------