diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..49ccc49 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: 🐛 Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Smartphone (please complete the following information):** +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a49eab2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..118d30e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: 🚀 Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5ca513..6e4ab89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,18 @@ name: Release -on: push +on: + push: + branches: + - '**' + tags-ignore: + - '**' + workflow_dispatch: + jobs: test: runs-on: ubuntu-18.04 strategy: matrix: - go: ['1.14', '1.15', '1.16'] + go: ['1.14', '1.15', '1.16', '1.17'] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v2 @@ -14,13 +21,14 @@ jobs: with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov ./pkg/... - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v2.1.0 with: file: ./profile.cov name: codecov-go release: runs-on: ubuntu-18.04 needs: [test] + if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: diff --git a/.releaserc.js b/.releaserc.js index d9c7f99..6500ace 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -1,8 +1,8 @@ module.exports = { - branch: 'master', + branches: ["main"], plugins: [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/github" ] - }; \ No newline at end of file +}; diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b107ae4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +abuse@zitadel.ch. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f0c8ac7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# How to contribute to the OIDC SDK for Go + +## Did you find a bug? + +Please file an issue [here](https://github.com/caos/oidc/issues/new?assignees=&labels=bug&template=bug_report.md&title=). + +Bugs are evaluated every day as soon as possible. + +## Enhancement + +Do you miss a feature? Please file an issue [here](https://github.com/caos/oidc/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) + +Enhancements are discussed and evaluated every Wednesday by the ZITADEL core team. + +## Grab an Issues + +We add the label "good first issue" for problems we think are a good starting point to contribute to the OIDC SDK. + +* [Issues for first time contributors](https://github.com/caos/oidc/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +* [All issues](https://github.com/caos/oidc/issues) + +### Make a PR + +If you like to contribute fork the OIDC repository. After you implemented the new feature create a PullRequest in the OIDC reposiotry. + +Make sure you use semantic release: + +* feat: New Feature +* fix: Bug Fix +* docs: Documentation + +## Want to use the library? + +Checkout the [examples folder](example) for different client and server implementations. + +Or checkout how we use it ourselves in our OpenSource Identity and Access Management [ZITADEL](https://github.com/caos/zitadel). + +## **Did you find a security flaw?** + +* Please read [Security Policy](SECURITY.md). \ No newline at end of file diff --git a/README.md b/README.md index f7b4365..ef428ed 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,13 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/caos/oidc)](https://goreportcard.com/report/github.com/caos/oidc) [![codecov](https://codecov.io/gh/caos/oidc/branch/master/graph/badge.svg)](https://codecov.io/gh/caos/oidc) -> This project is in beta state. It can AND will continue breaking until version 1.0.0 is released +![openid_certified](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png) ## What Is It -This project is a easy to use client and server implementation for the `OIDC` (Open ID Connect) standard written for `Go`. +This project is a easy to use client (RP) and server (OP) implementation for the `OIDC` (Open ID Connect) standard written for `Go`. + +The RP is certified for the [basic](https://www.certification.openid.net/plan-detail.html?public=true&plan=uoprP0OO8Z4Qo) and [config](https://www.certification.openid.net/plan-detail.html?public=true&plan=AYSdLbzmWbu9X) profile. Whenever possible we tried to reuse / extend existing packages like `OAuth2 for Go`. @@ -44,6 +46,7 @@ For your convenience you can find the relevant standards linked below. | 1.14 | :white_check_mark: | | 1.15 | :white_check_mark: | | 1.16 | :white_check_mark: | +| 1.17 | :white_check_mark: | ## Why another library @@ -51,7 +54,6 @@ As of 2020 there are not a lot of `OIDC` library's in `Go` which can handle serv ### Goals -- [Certify this library as RP](https://openid.net/certification/#RPs) - [Certify this library as OP](https://openid.net/certification/#OPs) ### Other Go OpenID Connect library's diff --git a/go.mod b/go.mod index e582c70..a340df8 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.2 // indirect github.com/google/go-github/v31 v31.0.0 - github.com/google/uuid v1.2.0 + github.com/google/uuid v1.3.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 @@ -16,7 +16,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.3.6 + golang.org/x/text v0.3.7 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/go.sum b/go.sum index d08c4eb..4ff0c39 100644 --- a/go.sum +++ b/go.sum @@ -50,9 +50,13 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -95,6 +99,7 @@ github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6C github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -105,8 +110,8 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= @@ -120,6 +125,7 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -129,21 +135,30 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= +github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -244,6 +259,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -272,8 +288,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -403,6 +419,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 98ed501..4062ab4 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -3,26 +3,41 @@ package rp import ( "context" "encoding/json" - "errors" "fmt" "net/http" "sync" "github.com/caos/oidc/pkg/utils" - "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" ) -func NewRemoteKeySet(client *http.Client, jwksURL string) oidc.KeySet { - return &remoteKeySet{httpClient: client, jwksURL: jwksURL} +func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet { + keyset := &remoteKeySet{httpClient: client, jwksURL: jwksURL} + for _, opt := range opts { + opt(keyset) + } + return keyset +} + +//SkipRemoteCheck will suppress checking for new remote keys if signature validation fails with cached keys +//and no kid header is set in the JWT +// +//this might be handy to save some unnecessary round trips in cases where the JWT does not contain a kid header and +//there is only a single remote key +//please notice that remote keys will then only be fetched if cached keys are empty +func SkipRemoteCheck() func(set *remoteKeySet) { + return func(set *remoteKeySet) { + set.skipRemoteCheck = true + } } type remoteKeySet struct { - jwksURL string - httpClient *http.Client - defaultAlg string + jwksURL string + httpClient *http.Client + defaultAlg string + skipRemoteCheck bool // guard all other fields mu sync.Mutex @@ -72,23 +87,67 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig if alg == "" { alg = r.defaultAlg } - keys := r.keysFromCache() - key, ok := oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keys...) - if ok && keyID != "" { - payload, err := jws.Verify(&key) - return payload, err + payload, err := r.verifySignatureCached(jws, keyID, alg) + if payload != nil { + return payload, nil } + if err != nil { + return nil, err + } + return r.verifySignatureRemote(ctx, jws, keyID, alg) +} +//verifySignatureCached checks for a matching key in the cached key list +// +//if there is only one possible, it tries to verify the signature and will return the payload if successful +// +//it only returns an error if signature validation fails and keys exactMatch which is if either: +// - both kid are empty and skipRemoteCheck is set to true +// - or both (JWT and JWK) kid are equal +// +//otherwise it will return no error (so remote keys will be loaded) +func (r *remoteKeySet) verifySignatureCached(jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) { + keys := r.keysFromCache() + if len(keys) == 0 { + return nil, nil + } + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keys...) + if err != nil { + //no key / multiple found, try with remote keys + return nil, nil //nolint:nilerr + } + payload, err := jws.Verify(&key) + if payload != nil { + return payload, nil + } + if !r.exactMatch(key.KeyID, keyID) { + //no exact key match, try getting better match with remote keys + return nil, nil + } + return nil, fmt.Errorf("signature verification failed: %w", err) +} + +func (r *remoteKeySet) exactMatch(jwkID, jwsID string) bool { + if jwkID == "" && jwsID == "" { + return r.skipRemoteCheck + } + return jwkID == jwsID +} + +func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) { keys, err := r.keysFromRemote(ctx) if err != nil { - return nil, fmt.Errorf("fetching keys %v", err) + return nil, fmt.Errorf("unable to fetch key for signature validation: %w", err) } - key, ok = oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keys...) - if ok { - payload, err := jws.Verify(&key) - return payload, err + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keys...) + if err != nil { + return nil, fmt.Errorf("unable to validate signature: %w", err) } - return nil, errors.New("invalid key") + payload, err := jws.Verify(&key) + if err != nil { + return nil, fmt.Errorf("signature verification failed: %w", err) + } + return payload, nil } func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey) { diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index c9c7a8c..669a910 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -2,6 +2,7 @@ package rp import ( "context" + "encoding/base64" "errors" "net/http" "strings" @@ -288,7 +289,7 @@ func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { //GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (string, error) { - codeVerifier := uuid.New().String() + codeVerifier := base64.RawURLEncoding.EncodeToString([]byte(uuid.New().String())) if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { return "", err } diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index adfffcf..3eca654 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" + "errors" "gopkg.in/square/go-jose.v2" ) @@ -13,6 +14,11 @@ const ( KeyUseSignature = "sig" ) +var ( + ErrKeyMultiple = errors.New("multiple possible keys match") + ErrKeyNone = errors.New("no possible keys matches") +) + //KeySet represents a set of JSON Web Keys // - remotely fetch via discovery and jwks_uri -> `remoteKeySet` // - held by the OP itself in storage -> `openIDKeySet` @@ -39,20 +45,38 @@ func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) { //will return the key immediately if matches exact (id, usage, type) // //will return false none or multiple match +// +//deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool +//moved implementation already to FindMatchingKey func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) { + key, err := FindMatchingKey(keyID, use, expectedAlg, keys...) + return key, err == nil +} + +//FindMatchingKey searches the given JSON Web Keys for the requested key ID, usage and key type +// +//will return the key immediately if matches exact (id, usage, type) +// +//will return a specific error if none (ErrKeyNone) or multiple (ErrKeyMultiple) match +func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (key jose.JSONWebKey, err error) { var validKeys []jose.JSONWebKey - for _, key := range keys { - if key.KeyID == keyID && key.Use == use && algToKeyType(key.Key, expectedAlg) { - if keyID != "" { - return key, true + for _, k := range keys { + if k.Use == use && algToKeyType(k.Key, expectedAlg) { + if k.KeyID == keyID && keyID != "" { + return k, nil + } + if k.KeyID == "" || keyID == "" { + validKeys = append(validKeys, k) } - validKeys = append(validKeys, key) } } if len(validKeys) == 1 { - return validKeys[0], true + return validKeys[0], nil } - return jose.JSONWebKey{}, false + if len(validKeys) > 1 { + return key, ErrKeyMultiple + } + return key, ErrKeyNone } func algToKeyType(key interface{}, alg string) bool { diff --git a/pkg/oidc/keyset_test.go b/pkg/oidc/keyset_test.go new file mode 100644 index 0000000..802edec --- /dev/null +++ b/pkg/oidc/keyset_test.go @@ -0,0 +1,319 @@ +package oidc + +import ( + "crypto/rsa" + "errors" + "reflect" + "testing" + + "gopkg.in/square/go-jose.v2" +) + +func TestFindKey(t *testing.T) { + type args struct { + keyID string + use string + expectedAlg string + keys []jose.JSONWebKey + } + type res struct { + key jose.JSONWebKey + err error + } + tests := []struct { + name string + args args + res res + }{ + { + "no keys, ErrKeyNone", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: nil, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "single key enc, ErrKeyNone", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "enc", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "single key wrong algorithm, ErrKeyNone", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PrivateKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "single key no kid, no jwt kid, match", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "single key kid, jwt no kid, match", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + KeyID: "id", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "single key no kid, jwt with kid, match", + args{ + keyID: "id", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "single key wrong kid, ErrKeyNone", + args{ + keyID: "id", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "multiple keys no kid, jwt no kid, ErrKeyMultiple", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys with kid, jwt no kid, ErrKeyMultiple", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys, single sig key, jwt no kid, match", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "enc", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "multiple keys no kid, jwt with kid, ErrKeyMultiple", + args{ + keyID: "id", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys with kid, jwt with kid, match", + args{ + keyID: "id1", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "multiple keys, single sig key, jwt with kid, match", + args{ + keyID: "id1", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "enc", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := FindMatchingKey(tt.args.keyID, tt.args.use, tt.args.expectedAlg, tt.args.keys...) + if (tt.res.err != nil && !errors.Is(err, tt.res.err)) || (tt.res.err == nil && err != nil) { + t.Errorf("FindKey() error, got = %v, want = %v", err, tt.res.err) + } + if !reflect.DeepEqual(got, tt.res.key) { + t.Errorf("FindKey() got = %v, want %v", got, tt.res.key) + } + }) + } +} diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index f8470b5..4284d17 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -39,6 +39,7 @@ var ( ErrSignatureMultiple = errors.New("id_token contains multiple signatures") ErrSignatureUnsupportedAlg = errors.New("signature algorithm not supported") ErrSignatureInvalidPayload = errors.New("signature does not match Payload") + ErrSignatureInvalid = errors.New("invalid signature") ErrExpired = errors.New("token has expired") ErrIatMissing = errors.New("issuedAt of token is missing") ErrIatInFuture = errors.New("issuedAt of token is in the future") @@ -143,7 +144,7 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl signedPayload, err := set.VerifySignature(ctx, jws) if err != nil { - return err + return fmt.Errorf("%w (%v)", ErrSignatureInvalid, err) } if !bytes.Equal(signedPayload, payload) { diff --git a/pkg/op/config.go b/pkg/op/config.go index 704cc20..39c84c8 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -4,7 +4,6 @@ import ( "errors" "net/url" "os" - "strings" "golang.org/x/text/language" ) @@ -57,9 +56,5 @@ func devLocalAllowed(url *url.URL) bool { if !b { return b } - return url.Scheme == "http" && - url.Host == "localhost" || - url.Host == "127.0.0.1" || - url.Host == "::1" || - strings.HasPrefix(url.Host, "localhost:") + return url.Scheme == "http" } diff --git a/pkg/op/op.go b/pkg/op/op.go index e10a088..419a9b9 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -2,7 +2,7 @@ package op import ( "context" - "errors" + "fmt" "net/http" "time" @@ -279,12 +279,12 @@ type openIDKeySet struct { func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { keySet, err := o.Storage.GetKeySet(ctx) if err != nil { - return nil, errors.New("error fetching keys") + return nil, fmt.Errorf("error fetching keys: %w", err) } keyID, alg := oidc.GetKeyIDAndAlg(jws) - key, ok := oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) - if !ok { - return nil, errors.New("invalid kid") + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) + if err != nil { + return nil, fmt.Errorf("invalid signature: %w", err) } return jws.Verify(&key) }