Compare commits

..

No commits in common. "main" and "v2.0.0-next.2" have entirely different histories.

169 changed files with 3572 additions and 14310 deletions

View file

@ -1,57 +0,0 @@
name: Bug Report
description: "Create a bug report to help us improve ZITADEL. Click [here](https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#product-management) to see how we process your issue."
title: "[Bug]: "
labels: ["bug"]
type: Bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: checkboxes
id: preflight
attributes:
label: Preflight Checklist
options:
- label:
I could not find a solution in the documentation, the existing issues or discussions
required: true
- label:
I have joined the [ZITADEL chat](https://zitadel.com/chat)
- type: input
id: version
attributes:
label: Version
description: Which version of the OIDC library are you using.
- type: textarea
id: impact
attributes:
label: Describe the problem caused by this bug
description: A clear and concise description of the problem you have and what the bug is.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: To reproduce
description: Steps to reproduce the behaviour
placeholder: |
Steps to reproduce the behavior:
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
- type: textarea
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
placeholder: As a [type of user], I want [some goal] so that [some reason].
- type: textarea
id: additional
attributes:
label: Additional Context
description: Please add any other infos that could be useful.

View file

@ -1,31 +0,0 @@
name: 📄 Documentation
description: Create an issue for missing or wrong documentation.
labels: ["docs"]
type: task
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this issue.
- type: checkboxes
id: preflight
attributes:
label: Preflight Checklist
options:
- label:
I could not find a solution in the existing issues, docs, nor discussions
required: true
- label:
I have joined the [ZITADEL chat](https://zitadel.com/chat)
- type: textarea
id: docs
attributes:
label: Describe the docs your are missing or that are wrong
placeholder: As a [type of user], I want [some goal] so that [some reason].
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional Context
description: Please add any other infos that could be useful.

View file

@ -1,55 +0,0 @@
name: 🛠️ Improvement
description: "Create an new issue for an improvment in ZITADEL"
labels: ["enhancement"]
type: enhancement
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this proposal / feature reqeust
- type: checkboxes
id: preflight
attributes:
label: Preflight Checklist
options:
- label:
I could not find a solution in the existing issues, docs, nor discussions
required: true
- label:
I have joined the [ZITADEL chat](https://zitadel.com/chat)
- type: textarea
id: problem
attributes:
label: Describe your problem
description: Please describe your problem this improvement is supposed to solve.
placeholder: Describe the problem you have
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe your ideal solution
description: Which solution do you propose?
placeholder: As a [type of user], I want [some goal] so that [some reason].
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Which version of the OIDC Library are you using.
- type: dropdown
id: environment
attributes:
label: Environment
description: How do you use ZITADEL?
options:
- ZITADEL Cloud
- Self-hosted
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional Context
description: Please add any other infos that could be useful.

View file

@ -1,16 +0,0 @@
### Definition of Ready
- [ ] I am happy with the code
- [ ] Short description of the feature/issue is added in the pr description
- [ ] PR is linked to the corresponding user story
- [ ] Acceptance criteria are met
- [ ] All open todos and follow ups are defined in a new ticket and justified
- [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented.
- [ ] No debug or dead code
- [ ] My code has no repetitions
- [ ] Critical parts are tested automatically
- [ ] Where possible E2E tests are implemented
- [ ] Documentation/examples are up-to-date
- [ ] All non-functional requirements are met
- [ ] Functionality of the acceptance criteria is checked manually on the dev system.

View file

@ -1,43 +0,0 @@
name: Add new issues to product management project
on:
issues:
types:
- opened
pull_request_target:
types:
- opened
jobs:
add-to-project:
name: Add issue and community pr to project
runs-on: ubuntu-latest
steps:
- name: add issue
uses: actions/add-to-project@v1.0.2
if: ${{ github.event_name == 'issues' }}
with:
# You can target a repository in a different organization
# to the issue
project-url: https://github.com/orgs/zitadel/projects/2
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
- uses: tspascoal/get-user-teams-membership@v3
id: checkUserMember
if: github.actor != 'dependabot[bot]'
with:
username: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }}
- name: add pr
uses: actions/add-to-project@v1.0.2
if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}}
with:
# You can target a repository in a different organization
# to the issue
project-url: https://github.com/orgs/zitadel/projects/2
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
- uses: actions-ecosystem/action-add-labels@v1.1.3
if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'staff')}}
with:
github_token: ${{ secrets.ADD_TO_PROJECT_PAT }}
labels: |
os-contribution

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -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.

View file

@ -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.

View file

@ -9,16 +9,6 @@ updates:
commit-message: commit-message:
prefix: chore prefix: chore
include: scope include: scope
- package-ecosystem: gomod
target-branch: "2.12.x"
directory: "/"
schedule:
interval: daily
time: '04:00'
open-pull-requests-limit: 10
commit-message:
prefix: chore
include: scope
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:

View file

@ -16,7 +16,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
@ -29,7 +29,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v2
# Override language selection by uncommenting this and choosing your languages # Override language selection by uncommenting this and choosing your languages
with: with:
languages: go languages: go
@ -37,7 +37,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -51,4 +51,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v2

18
.github/workflows/issue.yml vendored Normal file
View file

@ -0,0 +1,18 @@
name: Add new issues to product management project
on:
issues:
types:
- opened
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.4.0
with:
# You can target a repository in a different organization
# to the issue
project-url: https://github.com/orgs/zitadel/projects/2
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}

View file

@ -2,7 +2,6 @@ name: Release
on: on:
push: push:
branches: branches:
- "2.11.x"
- main - main
- next - next
tags-ignore: tags-ignore:
@ -14,34 +13,33 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-24.04 runs-on: ubuntu-20.04
strategy: strategy:
fail-fast: false
matrix: matrix:
go: ['1.23', '1.24'] go: ['1.16', '1.17', '1.18', '1.19', '1.20']
name: Go ${{ matrix.go }} test name: Go ${{ matrix.go }} test
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Setup go - name: Setup go
uses: actions/setup-go@v5 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - run: go test -race -v -coverprofile=profile.cov -coverpkg=github.com/zitadel/oidc/... ./pkg/...
- uses: codecov/codecov-action@v5.4.3 - uses: codecov/codecov-action@v3.1.1
with: with:
file: ./profile.cov file: ./profile.cov
name: codecov-go name: codecov-go
release: release:
runs-on: ubuntu-24.04 runs-on: ubuntu-20.04
needs: [test] needs: [test]
if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }} if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Source checkout - name: Source checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Semantic Release - name: Semantic Release
uses: cycjimmy/semantic-release-action@v4 uses: cycjimmy/semantic-release-action@v3
with: with:
dry_run: false dry_run: false
semantic_version: 18.0.1 semantic_version: 18.0.1

View file

@ -1,6 +1,5 @@
module.exports = { module.exports = {
branches: [ branches: [
{name: "2.11.x"},
{name: "main"}, {name: "main"},
{name: "next", prerelease: true}, {name: "next", prerelease: true},
], ],

112
README.md
View file

@ -2,13 +2,13 @@
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Release](https://github.com/zitadel/oidc/workflows/Release/badge.svg)](https://github.com/zitadel/oidc/actions) [![Release](https://github.com/zitadel/oidc/workflows/Release/badge.svg)](https://github.com/zitadel/oidc/actions)
[![Go Reference](https://pkg.go.dev/badge/github.com/zitadel/oidc/v3.svg)](https://pkg.go.dev/github.com/zitadel/oidc/v3) [![GoDoc](https://godoc.org/github.com/zitadel/oidc?status.png)](https://pkg.go.dev/github.com/zitadel/oidc)
[![license](https://badgen.net/github/license/zitadel/oidc/)](https://github.com/zitadel/oidc/blob/master/LICENSE) [![license](https://badgen.net/github/license/zitadel/oidc/)](https://github.com/zitadel/oidc/blob/master/LICENSE)
[![release](https://badgen.net/github/release/zitadel/oidc/stable)](https://github.com/zitadel/oidc/releases) [![release](https://badgen.net/github/release/zitadel/oidc/stable)](https://github.com/zitadel/oidc/releases)
[![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/oidc/v3)](https://goreportcard.com/report/github.com/zitadel/oidc/v3) [![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/oidc)](https://goreportcard.com/report/github.com/zitadel/oidc)
[![codecov](https://codecov.io/gh/zitadel/oidc/branch/main/graph/badge.svg)](https://codecov.io/gh/zitadel/oidc) [![codecov](https://codecov.io/gh/zitadel/oidc/branch/main/graph/badge.svg)](https://codecov.io/gh/zitadel/oidc)
[![openid_certified](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png)](https://openid.net/certification/) ![openid_certified](https://cloud.githubusercontent.com/assets/1454075/7611268/4d19de32-f97b-11e4-895b-31b2455a7ca6.png)
## What Is It ## What Is It
@ -21,10 +21,9 @@ Whenever possible we tried to reuse / extend existing packages like `OAuth2 for
## Basic Overview ## Basic Overview
The most important packages of the library: The most important packages of the library:
<pre> <pre>
/pkg /pkg
/client clients using the OP for retrieving, exchanging and verifying tokens /client clients using the OP for retrieving, exchanging and verifying tokens
/rp definition and implementation of an OIDC Relying Party (client) /rp definition and implementation of an OIDC Relying Party (client)
/rs definition and implementation of an OAuth Resource Server (API) /rs definition and implementation of an OAuth Resource Server (API)
/op definition and implementation of an OIDC OpenID Provider (server) /op definition and implementation of an OIDC OpenID Provider (server)
@ -38,10 +37,6 @@ The most important packages of the library:
/server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI /server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI
</pre> </pre>
### Semver
This package uses [semver](https://semver.org/) for [releases](https://github.com/zitadel/oidc/releases). Major releases ship breaking changes. Starting with the `v2` to `v3` increment we provide an [upgrade guide](UPGRADING.md) to ease migration to a newer version.
## How To Use It ## How To Use It
Check the `/example` folder where example code for different scenarios is located. Check the `/example` folder where example code for different scenarios is located.
@ -49,90 +44,33 @@ Check the `/example` folder where example code for different scenarios is locate
```bash ```bash
# start oidc op server # start oidc op server
# oidc discovery http://localhost:9998/.well-known/openid-configuration # oidc discovery http://localhost:9998/.well-known/openid-configuration
go run github.com/zitadel/oidc/v3/example/server go run github.com/zitadel/oidc/v2/example/server
# start oidc web client (in a new terminal) # start oidc web client (in a new terminal)
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app
``` ```
- open http://localhost:9999/login in your browser - open http://localhost:9999/login in your browser
- you will be redirected to op server and the login UI - you will be redirected to op server and the login UI
- login with user `test-user@localhost` and password `verysecure` - login with user `test-user@localhost` and password `verysecure`
- the OP will redirect you to the client app, which displays the user info - the OP will redirect you to the client app, which displays the user info
for the dynamic issuer, just start it with: for the dynamic issuer, just start it with:
```bash ```bash
go run github.com/zitadel/oidc/v3/example/server/dynamic go run github.com/zitadel/oidc/v2/example/server/dynamic
``` ```
the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with: the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with:
```bash ```bash
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app
``` ```
> Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`) > Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`)
### Server configuration
Example server allows extra configuration using environment variables and could be used for end to
end testing of your services.
| Name | Format | Description |
| ------------ | -------------------------------- | ------------------------------------- |
| PORT | Number between 1 and 65535 | OIDC listen port |
| REDIRECT_URI | Comma-separated URIs | List of allowed redirect URIs |
| USERS_FILE | Path to json in local filesystem | Users with their data and credentials |
Here is json equivalent for one of the default users
```json
{
"id2": {
"ID": "id2",
"Username": "test-user2",
"Password": "verysecure",
"FirstName": "Test",
"LastName": "User2",
"Email": "test-user2@zitadel.ch",
"EmailVerified": true,
"Phone": "",
"PhoneVerified": false,
"PreferredLanguage": "DE",
"IsAdmin": false
}
}
```
## Features ## Features
| | Relying party | OpenID Provider | Specification | | | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | Client Credentials |
| -------------------- | ------------- | --------------- | -------------------------------------------- | |------------------|-----------|---------------|-------------|-----------|------|----------------|---------|-------------|---------------|--------------------|
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] | | Relying Party | yes | no[^1] | no | yes | yes | partial | not yet | yes | yes | not yet |
| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] | | OpenID Provider | yes | yes | not yet | yes | yes | not yet | not yet | yes | yes | yes |
| Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] |
| Client Credentials | yes | yes | OpenID Connect Core 1.0, [Section 9][4] |
| Refresh Token | yes | yes | OpenID Connect Core 1.0, [Section 12][5] |
| Discovery | yes | yes | OpenID Connect [Discovery][6] 1.0 |
| JWT Profile | yes | yes | [RFC 7523][7] |
| PKCE | yes | yes | [RFC 7636][8] |
| Token Exchange | yes | yes | [RFC 8693][9] |
| Device Authorization | yes | yes | [RFC 8628][10] |
| mTLS | not yet | not yet | [RFC 8705][11] |
| Back-Channel Logout | not yet | yes | OpenID Connect [Back-Channel Logout][12] 1.0 |
[1]: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth "3.1. Authentication using the Authorization Code Flow"
[2]: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth "3.2. Authentication using the Implicit Flow"
[3]: https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth "3.3. Authentication using the Hybrid Flow"
[4]: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication "9. Client Authentication"
[5]: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens "12. Using Refresh Tokens"
[6]: https://openid.net/specs/openid-connect-discovery-1_0.html "OpenID Connect Discovery 1.0 incorporating errata set 1"
[7]: https://www.rfc-editor.org/rfc/rfc7523.html "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
[8]: https://www.rfc-editor.org/rfc/rfc7636.html "Proof Key for Code Exchange by OAuth Public Clients"
[9]: https://www.rfc-editor.org/rfc/rfc8693.html "OAuth 2.0 Token Exchange"
[10]: https://www.rfc-editor.org/rfc/rfc8628.html "OAuth 2.0 Device Authorization Grant"
[11]: https://www.rfc-editor.org/rfc/rfc8705.html "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
[12]: https://openid.net/specs/openid-connect-backchannel-1_0.html "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1"
## Contributors ## Contributors
@ -144,21 +82,28 @@ Made with [contrib.rocks](https://contrib.rocks).
### Resources ### Resources
For your convenience you can find the relevant guides linked below. For your convenience you can find the relevant standards linked below.
- [OpenID Connect Core 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-core-1_0.html) - [OpenID Connect Core 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-core-1_0.html)
- [OIDC/OAuth Flow in Zitadel (using this library)](https://zitadel.com/docs/guides/integrate/login-users) - [Proof Key for Code Exchange by OAuth Public Clients](https://tools.ietf.org/html/rfc7636)
- [OAuth 2.0 Token Exchange](https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-19)
- [OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-mtls-17)
- [JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7523)
- [OIDC/OAuth Flow in Zitadel (using this library)](https://docs.zitadel.com/docs/guides/integrate/login-users)
## Supported Go Versions ## Supported Go Versions
For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:). For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:).
Versions that also build are marked with :warning:. Versions that also build are marked with :warning:.
| Version | Supported | | Version | Supported |
| ------- | ------------------ | |---------|--------------------|
| <1.23 | :x: | | <1.16 | :x: |
| 1.23 | :white_check_mark: | | 1.16 | :warning: |
| 1.24 | :white_check_mark: | | 1.17 | :warning: |
| 1.18 | :warning: |
| 1.19 | :white_check_mark: |
| 1.20 | :white_check_mark: |
## Why another library ## Why another library
@ -189,4 +134,5 @@ Unless required by applicable law or agreed to in writing, software distributed
AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License. language governing permissions and limitations under the License.
[^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892 [^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892

View file

@ -1,20 +1,43 @@
# Security Policy # Security Policy
Please refer to the security policy [on zitadel/zitadel](https://github.com/zitadel/zitadel/blob/main/SECURITY.md) which is applicable for all open source repositories of our organization. At ZITADEL we are extremely grateful for security aware people that disclose vulnerabilities to us and the open source community. All reports will be investigated by our team.
## Supported Versions ## Supported Versions
We currently support the following version of the OIDC framework: After the initial Release the following version support will apply
| Version | Supported | Branch | Details | | Version | Supported |
| -------- | ------------------ | ----------- | ------------------------------------ | | ------- | ------------------ |
| 0.x.x | :x: | | not maintained | | 0.x.x | :x: |
| <2.11 | :x: | | not maintained | | 1.x.x | :white_check_mark: |
| 2.11.x | :lock: :warning: | [2.11.x][1] | security only, [community effort][2] | | 2.x.x | :white_check_mark: (not released) |
| 3.x.x | :heavy_check_mark: | [main][3] | supported |
| 4.0.0-xx | :white_check_mark: | [next][4] | [development branch] |
[1]: https://github.com/zitadel/oidc/tree/2.11.x ## Reporting a vulnerability
[2]: https://github.com/zitadel/oidc/discussions/458
[3]: https://github.com/zitadel/oidc/tree/main To file a incident, please disclose by email to security@zitadel.com with the security details.
[4]: https://github.com/zitadel/oidc/tree/next
At the moment GPG encryption is no yet supported, however you may sign your message at will.
### When should I report a vulnerability
* You think you discovered a ...
* ... potential security vulnerability in the SDK
* ... vulnerability in another project that this SDK bases on
* For projects with their own vulnerability reporting and disclosure process, please report it directly there
### When should I NOT report a vulnerability
* You need help applying security related updates
* Your issue is not security related
## Security Vulnerability Response
TBD
## Public Disclosure
All accepted and mitigated vulnerabilities will be published on the [Github Security Page](https://github.com/zitadel/oidc/security/advisories)
### Timing
We think it is crucial to publish advisories `ASAP` as mitigations are ready. But due to the unknown nature of the disclosures the time frame can range from 7 to 90 days.

View file

@ -1,370 +0,0 @@
# Upgrading
All commands are executed from the root of the project that imports oidc packages.
`sed` commands are created with **GNU sed** in mind and might need alternate syntax
on non-GNU systems, such as MacOS.
Alternatively, GNU sed can be installed on such systems. (`coreutils` package?).
## V2 to V3
**TL;DR** at the [bottom](#full-script) of this chapter is a full `sed` script
containing all automatic steps at once.
As first steps we will:
1. Download the latest v3 module;
2. Replace imports in all Go files;
3. Tidy the module file;
```bash
go get -u github.com/zitadel/oidc/v3
find . -type f -name '*.go' | xargs sed -i \
-e 's/github\.com\/zitadel\/oidc\/v2/github.com\/zitadel\/oidc\/v3/g'
go mod tidy
```
### global
#### go-jose package
`gopkg.in/square/go-jose.v2` import has been changed to `github.com/go-jose/go-jose/v3`.
That means that the imported types are also changed and imports need to be adapted.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/gopkg.in\/square\/go-jose\.v2/github.com\/go-jose\/go-jose\/v3/g'
go mod tidy
```
### op
```go
import "github.com/zitadel/oidc/v3/pkg/op"
```
#### Logger
This version of OIDC adds logging to the framework. For this we use the new Go standard library `log/slog`. (Until v3.12.0 we used `x/exp/slog`).
Mostly OIDC will use error level logs where it's returning an error through a HTTP handler. OIDC errors that are user facing don't carry much context, also for security reasons. With logging we are now able to print the error context, so that developers can more easily find the source of their issues. Previously we just discarded such context.
Most users of the OP package with the storage interface will not experience breaking changes. However if you use `RequestError()` directly in your code, you now need to give it a `Logger` as final argument.
The `OpenIDProvider` and sub-interfaces like `Authorizer` and `Exchanger` got a `Logger()` method to return the configured logger. This logger is in turn used by `AuthRequestError()`. You configure the logger with the `WithLogger()` for the `Provider`. By default the `slog.Default()` is used.
We also provide a new optional interface: [`LogAuthRequest`](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/op#LogAuthRequest). If an `AuthRequest` implements this interface, it is completely passed into the logger after an error. Its `LogValue()` will be used by `slog` to print desired fields. This allows omitting sensitive fields you wish not no print. If the interface is not implemented, no `AuthRequest` details will ever be printed.
#### Server interface
We've added a new [`Server`](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/op#Server) interface. This interface is experimental and subject to change. See [issue 440](https://github.com/zitadel/oidc/issues/440) for the motivation and discussion around this new interface.
Usage of the new interface is not required, but may be used for advanced scenarios when working with the `Storage` interface isn't the optimal solution for your app (like we experienced in [Zitadel](https://github.com/zitadel/zitadel)).
#### AuthRequestError
`AuthRequestError` now takes the complete `Authorizer` as final argument, instead of only the encoder.
This is to facilitate the use of the `Logger` as described above.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/\bAuthRequestError(w, r, authReq, err, authorizer.Encoder())/AuthRequestError(w, r, authReq, err, authorizer)/g'
```
Note: the sed regex might not find all uses if the local variables of the passed arguments use different names.
#### AccessTokenVerifier
`AccessTokenVerifier` interface has become a struct type. `NewAccessTokenVerifier` now returns a pointer to `AccessTokenVerifier`.
Variable and struct fields declarations need to be changed from `op.AccessTokenVerifier` to `*op.AccessTokenVerifier`.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/\bop\.AccessTokenVerifier\b/*op.AccessTokenVerifier/g'
```
#### JWTProfileVerifier
`JWTProfileVerifier` interface has become a struct type. `NewJWTProfileVerifier` now returns a pointer to `JWTProfileVerifier`.
Variable and struct fields declarations need to be changed from `op.JWTProfileVerifier` to `*op.JWTProfileVerifier`.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/\bop\.JWTProfileVerifier\b/*op.JWTProfileVerifier/g'
```
#### IDTokenHintVerifier
`IDTokenHintVerifier` interface has become a struct type. `NewIDTokenHintVerifier` now returns a pointer to `IDTokenHintVerifier`.
Variable and struct fields declarations need to be changed from `op.IDTokenHintVerifier` to `*op.IDTokenHintVerifier`.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/\bop\.IDTokenHintVerifier\b/*op.IDTokenHintVerifier/g'
```
#### ParseRequestObject
`ParseRequestObject` no longer returns `*oidc.AuthRequest` as it already operates on the pointer for the passed `authReq` argument. As such the argument and the return value were the same pointer. Callers can just use the original `*oidc.AuthRequest` now.
#### Endpoint Configuration
`Endpoint`s returned from `Configuration` interface methods are now pointers. Usually, `op.Provider` is the main implementation of the `Configuration` interface. However, if a custom implementation is used, you should be able to update it using the following:
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/AuthorizationEndpoint() Endpoint/AuthorizationEndpoint() *Endpoint/g' \
-e 's/TokenEndpoint() Endpoint/TokenEndpoint() *Endpoint/g' \
-e 's/IntrospectionEndpoint() Endpoint/IntrospectionEndpoint() *Endpoint/g' \
-e 's/UserinfoEndpoint() Endpoint/UserinfoEndpoint() *Endpoint/g' \
-e 's/RevocationEndpoint() Endpoint/RevocationEndpoint() *Endpoint/g' \
-e 's/EndSessionEndpoint() Endpoint/EndSessionEndpoint() *Endpoint/g' \
-e 's/KeysEndpoint() Endpoint/KeysEndpoint() *Endpoint/g' \
-e 's/DeviceAuthorizationEndpoint() Endpoint/DeviceAuthorizationEndpoint() *Endpoint/g'
```
#### CreateDiscoveryConfig
`CreateDiscoveryConfig` now takes a context as first argument. The following adds `context.TODO()` to the function:
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/op\.CreateDiscoveryConfig(/op.CreateDiscoveryConfig(context.TODO(), /g'
```
It now takes the issuer out of the context using the [`IssuerFromContext`](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/op#IssuerFromContext) functionality,
instead of the `config.IssuerFromRequest()` method.
#### CreateRouter
`CreateRouter` now returns a `chi.Router` instead of `*mux.Router`.
Usually this function is called when the Provider is constructed and not by package consumers.
However if your project does call this function directly, manual update of the code is required.
#### DeviceAuthorizationStorage
`DeviceAuthorizationStorage` dropped the following methods:
- `GetDeviceAuthorizationByUserCode`
- `CompleteDeviceAuthorization`
- `DenyDeviceAuthorization`
These methods proved not to be required from a library point of view.
Implementations of a device authorization flow may take care of these calls in a way they see fit.
#### AuthorizeCodeChallenge
The `AuthorizeCodeChallenge` function now only takes the `CodeVerifier` argument, instead of the complete `*oidc.AccessTokenRequest`.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/op\.AuthorizeCodeChallenge(tokenReq/op.AuthorizeCodeChallenge(tokenReq.CodeVerifier/g'
```
### client
```go
import "github.com/zitadel/oidc/v3/pkg/client"
```
#### Context
All client calls now take a context as first argument. The following adds `context.TODO()` to all the affected functions:
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/client\.Discover(/client.Discover(context.TODO(), /g' \
-e 's/client\.CallTokenEndpoint(/client.CallTokenEndpoint(context.TODO(), /g' \
-e 's/client\.CallEndSessionEndpoint(/client.CallEndSessionEndpoint(context.TODO(), /g' \
-e 's/client\.CallRevokeEndpoint(/client.CallRevokeEndpoint(context.TODO(), /g' \
-e 's/client\.CallTokenExchangeEndpoint(/client.CallTokenExchangeEndpoint(context.TODO(), /g' \
-e 's/client\.CallDeviceAuthorizationEndpoint(/client.CallDeviceAuthorizationEndpoint(context.TODO(), /g' \
-e 's/client\.JWTProfileExchange(/client.JWTProfileExchange(context.TODO(), /g'
```
#### keyFile type
The `keyFile` struct type is now exported a `KeyFile` and returned by the `ConfigFromKeyFile` and `ConfigFromKeyFileData`. No changes are needed on the caller's side.
### client/profile
The package now defines a new interface `TokenSource` which compliments the `oauth2.TokenSource` with a `TokenCtx` method, so that a context can be explicitly added on each call. Users can migrate to the new method when they whish.
`NewJWTProfileTokenSource` now takes a context as first argument, so do the related `NewJWTProfileTokenSourceFromKeyFile` and `NewJWTProfileTokenSourceFromKeyFileData`. The context is used for the Discovery request.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/profile\.NewJWTProfileTokenSource(/profile.NewJWTProfileTokenSource(context.TODO(), /g' \
-e 's/profile\.NewJWTProfileTokenSourceFromKeyFileData(/profile.NewJWTProfileTokenSourceFromKeyFileData(context.TODO(), /g' \
-e 's/profile\.NewJWTProfileTokenSourceFromKeyFile(/profile.NewJWTProfileTokenSourceFromKeyFile(context.TODO(), /g'
```
### client/rp
```go
import "github.com/zitadel/oidc/v3/pkg/client/rs"
```
#### Discover
The `Discover` function has been removed. Use `client.Discover` instead.
#### Context
Most `rp` functions now require a context as first argument. The following adds `context.TODO()` to the function that have no additional changes. Functions with more complex changes are documented below.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/rp\.NewRelyingPartyOIDC(/rp.NewRelyingPartyOIDC(context.TODO(), /g' \
-e 's/rp\.EndSession(/rp.EndSession(context.TODO(), /g' \
-e 's/rp\.RevokeToken(/rp.RevokeToken(context.TODO(), /g' \
-e 's/rp\.DeviceAuthorization(/rp.DeviceAuthorization(context.TODO(), /g'
```
Remember to replace `context.TODO()` with a context that is applicable for your app, where possible.
#### RefreshAccessToken
1. Renamed to `RefreshTokens`;
2. A context must be passed;
3. An `*oidc.Tokens` object is now returned, which included an ID Token if it was returned by the server;
4. The function is now generic and requires a type argument for the `IDTokenClaims` implementation inside the returned `oidc.Tokens` object;
For most use cases `*oidc.IDTokenClaims` can be used as type argument. A custom implementation of `oidc.IDClaims` can be used if type-safe access to custom claims is required.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/rp\.RefreshAccessToken(/rp.RefreshTokens[*oidc.IDTokenClaims](context.TODO(), /g'
```
Users that called `tokens.Extra("id_token").(string)` and a subsequent `VerifyTokens` to get the claims, no longer need to do this. The ID token is verified (when present) by `RefreshTokens` already.
#### Userinfo
1. A context must be passed as first argument;
2. The function is now generic and requires a type argument for the returned user info object;
For most use cases `*oidc.UserInfo` can be used a type argument. A [custom implementation](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/client/rp#example-Userinfo-Custom) of `rp.SubjectGetter` can be used if type-safe access to custom claims is required.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/rp\.Userinfo(/rp.Userinfo[*oidc.UserInfo](context.TODO(), /g'
```
#### UserinfoCallback
`UserinfoCallback` has an additional type argument fot the `UserInfo` object. Typically the type argument can be inferred by the compiler, by the function that is passed. The actual code update cannot be done by a simple `sed` script and depends on how the caller implemented the function.
#### IDTokenVerifier
`IDTokenVerifier` interface has become a struct type. `NewIDTokenVerifier` now returns a pointer to `IDTokenVerifier`.
Variable and struct fields declarations need to be changed from `rp.IDTokenVerifier` to `*rp.AccessTokenVerifier`.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/\brp\.IDTokenVerifier\b/*rp.IDTokenVerifier/g'
```
### client/rs
```go
import "github.com/zitadel/oidc/v3/pkg/client/rs"
```
#### NewResourceServer
The `NewResourceServerClientCredentials` and `NewResourceServerJWTProfile` constructor functions now take a context as first argument.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/rs\.NewResourceServerClientCredentials(/rs.NewResourceServerClientCredentials(context.TODO(), /g' \
-e 's/rs\.NewResourceServerJWTProfile(/rs.NewResourceServerJWTProfile(context.TODO(), /g'
```
#### Introspect
`Introspect` is now generic and requires a type argument for the returned introspection response. For most use cases `*oidc.IntrospectionResponse` can be used as type argument. Any other response type if type-safe access to [custom claims](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/client/rs#example-Introspect-Custom) is required.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/rs\.Introspect(/rs.Introspect[*oidc.IntrospectionResponse](/g'
```
### client/tokenexchange
The `TokenExchanger` constructor functions `NewTokenExchanger` and `NewTokenExchangerClientCredentials` now take a context as first argument.
As well as the `ExchangeToken` function.
```bash
find . -type f -name '*.go' | xargs sed -i \
-e 's/tokenexchange\.NewTokenExchanger(/tokenexchange.NewTokenExchanger(context.TODO(), /g' \
-e 's/tokenexchange\.NewTokenExchangerClientCredentials(/tokenexchange.NewTokenExchangerClientCredentials(context.TODO(), /g' \
-e 's/tokenexchange\.ExchangeToken(/tokenexchange.ExchangeToken(context.TODO(), /g'
```
### oidc
#### SpaceDelimitedArray
The `SpaceDelimitedArray` type's `Encode()` function has been renamed to `String()` so it implements the `fmt.Stringer` interface. If the `Encode` method was called by a package consumer, it should be changed manually.
#### Verifier
The `Verifier` interface as been changed into a struct type. The struct type is aliased in the `op` and `rp` packages for the specific token use cases. See the relevant section above.
### Full script
For the courageous this is the full `sed` script which combines all the steps described above.
It should migrate most of the code in a repository to a more-or-less compilable state,
using defaults such as `context.TODO()` where possible.
Warnings:
- Again, this is written for **GNU sed** not the posix variant.
- Assumes imports that use the package names, not aliases.
- Do this on a project with version control (eg Git), that allows you to rollback if things went wrong.
- The script has been tested on the [ZITADEL](https://github.com/zitadel/zitadel) project, but we do not use all affected symbols. Parts of the script are mere guesswork.
```bash
go get -u github.com/zitadel/oidc/v3
find . -type f -name '*.go' | xargs sed -i \
-e 's/github\.com\/zitadel\/oidc\/v2/github.com\/zitadel\/oidc\/v3/g' \
-e 's/gopkg.in\/square\/go-jose\.v2/github.com\/go-jose\/go-jose\/v3/g' \
-e 's/\bAuthRequestError(w, r, authReq, err, authorizer.Encoder())/AuthRequestError(w, r, authReq, err, authorizer)/g' \
-e 's/\bop\.AccessTokenVerifier\b/*op.AccessTokenVerifier/g' \
-e 's/\bop\.JWTProfileVerifier\b/*op.JWTProfileVerifier/g' \
-e 's/\bop\.IDTokenHintVerifier\b/*op.IDTokenHintVerifier/g' \
-e 's/AuthorizationEndpoint() Endpoint/AuthorizationEndpoint() *Endpoint/g' \
-e 's/TokenEndpoint() Endpoint/TokenEndpoint() *Endpoint/g' \
-e 's/IntrospectionEndpoint() Endpoint/IntrospectionEndpoint() *Endpoint/g' \
-e 's/UserinfoEndpoint() Endpoint/UserinfoEndpoint() *Endpoint/g' \
-e 's/RevocationEndpoint() Endpoint/RevocationEndpoint() *Endpoint/g' \
-e 's/EndSessionEndpoint() Endpoint/EndSessionEndpoint() *Endpoint/g' \
-e 's/KeysEndpoint() Endpoint/KeysEndpoint() *Endpoint/g' \
-e 's/DeviceAuthorizationEndpoint() Endpoint/DeviceAuthorizationEndpoint() *Endpoint/g' \
-e 's/op\.CreateDiscoveryConfig(/op.CreateDiscoveryConfig(context.TODO(), /g' \
-e 's/op\.AuthorizeCodeChallenge(tokenReq/op.AuthorizeCodeChallenge(tokenReq.CodeVerifier/g' \
-e 's/client\.Discover(/client.Discover(context.TODO(), /g' \
-e 's/client\.CallTokenEndpoint(/client.CallTokenEndpoint(context.TODO(), /g' \
-e 's/client\.CallEndSessionEndpoint(/client.CallEndSessionEndpoint(context.TODO(), /g' \
-e 's/client\.CallRevokeEndpoint(/client.CallRevokeEndpoint(context.TODO(), /g' \
-e 's/client\.CallTokenExchangeEndpoint(/client.CallTokenExchangeEndpoint(context.TODO(), /g' \
-e 's/client\.CallDeviceAuthorizationEndpoint(/client.CallDeviceAuthorizationEndpoint(context.TODO(), /g' \
-e 's/client\.JWTProfileExchange(/client.JWTProfileExchange(context.TODO(), /g' \
-e 's/profile\.NewJWTProfileTokenSource(/profile.NewJWTProfileTokenSource(context.TODO(), /g' \
-e 's/profile\.NewJWTProfileTokenSourceFromKeyFileData(/profile.NewJWTProfileTokenSourceFromKeyFileData(context.TODO(), /g' \
-e 's/profile\.NewJWTProfileTokenSourceFromKeyFile(/profile.NewJWTProfileTokenSourceFromKeyFile(context.TODO(), /g' \
-e 's/rp\.NewRelyingPartyOIDC(/rp.NewRelyingPartyOIDC(context.TODO(), /g' \
-e 's/rp\.EndSession(/rp.EndSession(context.TODO(), /g' \
-e 's/rp\.RevokeToken(/rp.RevokeToken(context.TODO(), /g' \
-e 's/rp\.DeviceAuthorization(/rp.DeviceAuthorization(context.TODO(), /g' \
-e 's/rp\.RefreshAccessToken(/rp.RefreshTokens[*oidc.IDTokenClaims](context.TODO(), /g' \
-e 's/rp\.Userinfo(/rp.Userinfo[*oidc.UserInfo](context.TODO(), /g' \
-e 's/\brp\.IDTokenVerifier\b/*rp.IDTokenVerifier/g' \
-e 's/rs\.NewResourceServerClientCredentials(/rs.NewResourceServerClientCredentials(context.TODO(), /g' \
-e 's/rs\.NewResourceServerJWTProfile(/rs.NewResourceServerJWTProfile(context.TODO(), /g' \
-e 's/rs\.Introspect(/rs.Introspect[*oidc.IntrospectionResponse](/g' \
-e 's/tokenexchange\.NewTokenExchanger(/tokenexchange.NewTokenExchanger(context.TODO(), /g' \
-e 's/tokenexchange\.NewTokenExchangerClientCredentials(/tokenexchange.NewTokenExchangerClientCredentials(context.TODO(), /g' \
-e 's/tokenexchange\.ExchangeToken(/tokenexchange.ExchangeToken(context.TODO(), /g'
go mod tidy
```

View file

@ -1,7 +1,6 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
@ -10,11 +9,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/gorilla/mux"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs" "github.com/zitadel/oidc/v2/pkg/client/rs"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
const ( const (
@ -28,12 +27,12 @@ func main() {
port := os.Getenv("PORT") port := os.Getenv("PORT")
issuer := os.Getenv("ISSUER") issuer := os.Getenv("ISSUER")
provider, err := rs.NewResourceServerFromKeyFile(context.TODO(), issuer, keyPath) provider, err := rs.NewResourceServerFromKeyFile(issuer, keyPath)
if err != nil { if err != nil {
logrus.Fatalf("error creating provider %s", err.Error()) logrus.Fatalf("error creating provider %s", err.Error())
} }
router := chi.NewRouter() router := mux.NewRouter()
// public url accessible without any authorization // public url accessible without any authorization
// will print `OK` and current timestamp // will print `OK` and current timestamp
@ -48,7 +47,7 @@ func main() {
if !ok { if !ok {
return return
} }
resp, err := rs.Introspect[*oidc.IntrospectionResponse](r.Context(), provider, token) resp, err := rs.Introspect(r.Context(), provider, token)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusForbidden) http.Error(w, err.Error(), http.StatusForbidden)
return return
@ -69,15 +68,15 @@ func main() {
if !ok { if !ok {
return return
} }
resp, err := rs.Introspect[*oidc.IntrospectionResponse](r.Context(), provider, token) resp, err := rs.Introspect(r.Context(), provider, token)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusForbidden) http.Error(w, err.Error(), http.StatusForbidden)
return return
} }
requestedClaim := chi.URLParam(r, "claim") params := mux.Vars(r)
requestedValue := chi.URLParam(r, "value") requestedClaim := params["claim"]
requestedValue := params["value"]
value, ok := resp.Claims[requestedClaim].(string) value, ok := resp.GetClaim(requestedClaim).(string)
if !ok || value == "" || value != requestedValue { if !ok || value == "" || value != requestedValue {
http.Error(w, "claim does not match", http.StatusForbidden) http.Error(w, "claim does not match", http.StatusForbidden)
return return

View file

@ -1,23 +1,19 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"sync/atomic"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/client/rp"
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/logging"
) )
var ( var (
@ -32,31 +28,13 @@ func main() {
issuer := os.Getenv("ISSUER") issuer := os.Getenv("ISSUER")
port := os.Getenv("PORT") port := os.Getenv("PORT")
scopes := strings.Split(os.Getenv("SCOPES"), " ") scopes := strings.Split(os.Getenv("SCOPES"), " ")
responseMode := os.Getenv("RESPONSE_MODE")
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
logger := slog.New(
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}),
)
client := &http.Client{
Timeout: time.Minute,
}
// enable outgoing request logging
logging.EnableHTTPClient(client,
logging.WithClientGroup("client"),
)
options := []rp.Option{ options := []rp.Option{
rp.WithCookieHandler(cookieHandler), rp.WithCookieHandler(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)), rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
rp.WithHTTPClient(client),
rp.WithLogger(logger),
rp.WithSigningAlgsFromDiscovery(),
} }
if clientSecret == "" { if clientSecret == "" {
options = append(options, rp.WithPKCE(cookieHandler)) options = append(options, rp.WithPKCE(cookieHandler))
@ -65,10 +43,7 @@ func main() {
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath))) options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
} }
// One can add a logger to the context, provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...)
// pre-defining log attributes as required.
ctx := logging.ToContext(context.TODO(), logger)
provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, redirectURI, scopes, options...)
if err != nil { if err != nil {
logrus.Fatalf("error creating provider %s", err.Error()) logrus.Fatalf("error creating provider %s", err.Error())
} }
@ -79,37 +54,18 @@ func main() {
return uuid.New().String() return uuid.New().String()
} }
urlOptions := []rp.URLParamOpt{ // register the AuthURLHandler at your preferred path
rp.WithPromptURLParam("Welcome back!"), // the AuthURLHandler creates the auth request and redirects the user to the auth server
} // including state handling with secure cookie and the possibility to use PKCE
http.Handle("/login", rp.AuthURLHandler(state, provider))
if responseMode != "" {
urlOptions = append(urlOptions, rp.WithResponseModeURLParam(oidc.ResponseMode(responseMode)))
}
// register the AuthURLHandler at your preferred path.
// the AuthURLHandler creates the auth request and redirects the user to the auth server.
// including state handling with secure cookie and the possibility to use PKCE.
// Prompts can optionally be set to inform the server of
// any messages that need to be prompted back to the user.
http.Handle("/login", rp.AuthURLHandler(
state,
provider,
urlOptions...,
))
// for demonstration purposes the returned userinfo response is written as JSON object onto response // for demonstration purposes the returned userinfo response is written as JSON object onto response
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
fmt.Println("access token", tokens.AccessToken)
fmt.Println("refresh token", tokens.RefreshToken)
fmt.Println("id token", tokens.IDToken)
data, err := json.Marshal(info) data, err := json.Marshal(info)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
w.Header().Set("content-type", "application/json")
w.Write(data) w.Write(data)
} }
@ -160,22 +116,8 @@ func main() {
// //
// http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider)) // http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider))
// simple counter for request IDs
var counter atomic.Int64
// enable incomming request logging
mw := logging.Middleware(
logging.WithLogger(logger),
logging.WithGroup("server"),
logging.WithIDFunc(func() slog.Attr {
return slog.Int64("id", counter.Add(1))
}),
)
lis := fmt.Sprintf("127.0.0.1:%s", port) lis := fmt.Sprintf("127.0.0.1:%s", port)
logger.Info("server listening, press ctrl+c to stop", "addr", lis) logrus.Infof("listening on http://%s/", lis)
err = http.ListenAndServe(lis, mw(http.DefaultServeMux)) logrus.Info("press ctrl+c to stop")
if err != http.ErrServerClosed { logrus.Fatal(http.ListenAndServe(lis, nil))
logger.Error("server terminated", "error", err)
os.Exit(1)
}
} }

View file

@ -1,37 +1,3 @@
// Command device is an example Oauth2 Device Authorization Grant app.
// It creates a new Device Authorization request on the Issuer and then polls for tokens.
// The user is then prompted to visit a URL and enter the user code.
// Or, the complete URL can be used instead to omit manual entry.
// In practice then can be a "magic link" in the form or a QR.
//
// The following environment variables are used for configuration:
//
// ISSUER: URL to the OP, required.
// CLIENT_ID: ID of the application, required.
// CLIENT_SECRET: Secret to authenticate the app using basic auth. Only required if the OP expects this type of authentication.
// KEY_PATH: Path to a private key file, used to for JWT authentication of the App. Only required if the OP expects this type of authentication.
// SCOPES: Scopes of the Authentication Request. Optional.
//
// Basic usage:
//
// cd example/client/device
// export ISSUER="http://localhost:9000" CLIENT_ID="246048465824634593@demo"
//
// Get an Access Token:
//
// SCOPES="email profile" go run .
//
// Get an Access Token and ID Token:
//
// SCOPES="email profile openid" go run .
//
// Get an Access Token and Refresh Token
//
// SCOPES="email profile offline_access" go run .
//
// Get Access, Refresh and ID Tokens:
//
// SCOPES="email profile offline_access openid" go run .
package main package main
import ( import (
@ -45,8 +11,8 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/client/rp"
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
) )
var ( var (
@ -73,13 +39,13 @@ func main() {
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath))) options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
} }
provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, "", scopes, options...) provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, "", scopes, options...)
if err != nil { if err != nil {
logrus.Fatalf("error creating provider %s", err.Error()) logrus.Fatalf("error creating provider %s", err.Error())
} }
logrus.Info("starting device authorization flow") logrus.Info("starting device authorization flow")
resp, err := rp.DeviceAuthorization(ctx, scopes, provider, nil) resp, err := rp.DeviceAuthorization(scopes, provider)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -91,5 +57,5 @@ func main() {
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Infof("successfully obtained token: %#v", token) logrus.Infof("successfully obtained token: %v", token)
} }

View file

@ -10,10 +10,9 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
githubOAuth "golang.org/x/oauth2/github" githubOAuth "golang.org/x/oauth2/github"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/client/rp"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp/cli" "github.com/zitadel/oidc/v2/pkg/client/rp/cli"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" "github.com/zitadel/oidc/v2/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
var ( var (
@ -44,7 +43,7 @@ func main() {
state := func() string { state := func() string {
return uuid.New().String() return uuid.New().String()
} }
token := cli.CodeFlow[*oidc.IDTokenClaims](ctx, relyingParty, callbackPath, port, state) token := cli.CodeFlow(ctx, relyingParty, callbackPath, port, state)
client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token)) client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token))

View file

@ -13,7 +13,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/profile" "github.com/zitadel/oidc/v2/pkg/client/profile"
) )
var client = http.DefaultClient var client = http.DefaultClient
@ -25,7 +25,7 @@ func main() {
scopes := strings.Split(os.Getenv("SCOPES"), " ") scopes := strings.Split(os.Getenv("SCOPES"), " ")
if keyPath != "" { if keyPath != "" {
ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(context.TODO(), issuer, keyPath, scopes) ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath, scopes)
if err != nil { if err != nil {
logrus.Fatalf("error creating token source %s", err.Error()) logrus.Fatalf("error creating token source %s", err.Error())
} }
@ -76,7 +76,7 @@ func main() {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(context.TODO(), issuer, key, scopes) ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(issuer, key, scopes)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -125,7 +125,7 @@ func main() {
testURL := r.Form.Get("url") testURL := r.Form.Get("url")
var data struct { var data struct {
URL string URL string
Response any Response interface{}
} }
if testURL != "" { if testURL != "" {
data.URL = testURL data.URL = testURL
@ -149,7 +149,7 @@ func main() {
logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil))
} }
func callExampleEndpoint(client *http.Client, testURL string) (any, error) { func callExampleEndpoint(client *http.Client, testURL string) (interface{}, error) {
req, err := http.NewRequest("GET", testURL, nil) req, err := http.NewRequest("GET", testURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -1,40 +0,0 @@
package config
import (
"os"
"strings"
)
const (
// default port for the http server to run
DefaultIssuerPort = "9998"
)
type Config struct {
Port string
RedirectURI []string
UsersFile string
}
// FromEnvVars loads configuration parameters from environment variables.
// If there is no such variable defined, then use default values.
func FromEnvVars(defaults *Config) *Config {
if defaults == nil {
defaults = &Config{}
}
cfg := &Config{
Port: defaults.Port,
RedirectURI: defaults.RedirectURI,
UsersFile: defaults.UsersFile,
}
if value, ok := os.LookupEnv("PORT"); ok {
cfg.Port = value
}
if value, ok := os.LookupEnv("USERS_FILE"); ok {
cfg.UsersFile = value
}
if value, ok := os.LookupEnv("REDIRECT_URI"); ok {
cfg.RedirectURI = strings.Split(value, ",")
}
return cfg
}

View file

@ -1,77 +0,0 @@
package config
import (
"fmt"
"os"
"testing"
)
func TestFromEnvVars(t *testing.T) {
for _, tc := range []struct {
name string
env map[string]string
defaults *Config
want *Config
}{
{
name: "no vars, no default values",
env: map[string]string{},
want: &Config{},
},
{
name: "no vars, only defaults",
env: map[string]string{},
defaults: &Config{
Port: "6666",
UsersFile: "/default/user/path",
RedirectURI: []string{"re", "direct", "uris"},
},
want: &Config{
Port: "6666",
UsersFile: "/default/user/path",
RedirectURI: []string{"re", "direct", "uris"},
},
},
{
name: "overriding default values",
env: map[string]string{
"PORT": "1234",
"USERS_FILE": "/path/to/users",
"REDIRECT_URI": "http://redirect/redirect",
},
defaults: &Config{
Port: "6666",
UsersFile: "/default/user/path",
RedirectURI: []string{"re", "direct", "uris"},
},
want: &Config{
Port: "1234",
UsersFile: "/path/to/users",
RedirectURI: []string{"http://redirect/redirect"},
},
},
{
name: "multiple redirect uris",
env: map[string]string{
"REDIRECT_URI": "http://host_1,http://host_2,http://host_3",
},
want: &Config{
RedirectURI: []string{
"http://host_1", "http://host_2", "http://host_3",
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
os.Clearenv()
for k, v := range tc.env {
os.Setenv(k, v)
}
cfg := FromEnvVars(tc.defaults)
if fmt.Sprint(cfg) != fmt.Sprint(tc.want) {
t.Errorf("Expected FromEnvVars()=%q, but got %q", tc.want, cfg)
}
})
}
}

View file

@ -6,9 +6,9 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"github.com/go-chi/chi/v5" "github.com/gorilla/mux"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
const ( const (
@ -43,7 +43,7 @@ var (
type login struct { type login struct {
authenticate authenticate authenticate authenticate
router chi.Router router *mux.Router
callback func(context.Context, string) string callback func(context.Context, string) string
} }
@ -57,9 +57,9 @@ func NewLogin(authenticate authenticate, callback func(context.Context, string)
} }
func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) { func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) {
l.router = chi.NewRouter() l.router = mux.NewRouter()
l.router.Get("/username", l.loginHandler) l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler)
l.router.With(issuerInterceptor.Handler).Post("/username", l.checkLoginHandler) l.router.Path("/username").Methods("POST").HandlerFunc(issuerInterceptor.HandlerFunc(l.checkLoginHandler))
} }
type authenticate interface { type authenticate interface {

View file

@ -7,11 +7,11 @@ import (
"log" "log"
"net/http" "net/http"
"github.com/go-chi/chi/v5" "github.com/gorilla/mux"
"golang.org/x/text/language" "golang.org/x/text/language"
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage" "github.com/zitadel/oidc/v2/example/server/storage"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
const ( const (
@ -47,7 +47,7 @@ func main() {
//be sure to create a proper crypto random key and manage it securely! //be sure to create a proper crypto random key and manage it securely!
key := sha256.Sum256([]byte("test")) key := sha256.Sum256([]byte("test"))
router := chi.NewRouter() router := mux.NewRouter()
//for simplicity, we provide a very small default page for users who have signed out //for simplicity, we provide a very small default page for users who have signed out
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
@ -76,7 +76,7 @@ func main() {
//regardless of how many pages / steps there are in the process, the UI must be registered in the router, //regardless of how many pages / steps there are in the process, the UI must be registered in the router,
//so we will direct all calls to /login to the login UI //so we will direct all calls to /login to the login UI
router.Mount("/login/", http.StripPrefix("/login", l.router)) router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router))
//we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) //we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
//is served on the correct path //is served on the correct path
@ -84,7 +84,7 @@ func main() {
//if your issuer ends with a path (e.g. http://localhost:9998/custom/path/), //if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
//then you would have to set the path prefix (/custom/path/): //then you would have to set the path prefix (/custom/path/):
//router.PathPrefix("/custom/path/").Handler(http.StripPrefix("/custom/path", provider.HttpHandler())) //router.PathPrefix("/custom/path/").Handler(http.StripPrefix("/custom/path", provider.HttpHandler()))
router.Mount("/", provider) router.PathPrefix("/").Handler(provider.HttpHandler())
server := &http.Server{ server := &http.Server{
Addr: ":" + port, Addr: ":" + port,

View file

@ -1,34 +1,21 @@
package exampleop package exampleop
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/gorilla/mux"
"github.com/go-chi/chi/v5"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/zitadel/oidc/v2/pkg/op"
) )
type deviceAuthenticate interface { type deviceAuthenticate interface {
CheckUsernamePasswordSimple(username, password string) error CheckUsernamePasswordSimple(username, password string) error
op.DeviceAuthorizationStorage op.DeviceAuthorizationStorage
// GetDeviceAuthorizationByUserCode resturns the current state of the device authorization flow,
// identified by the user code.
GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error)
// CompleteDeviceAuthorization marks a device authorization entry as Completed,
// identified by userCode. The Subject is added to the state, so that
// GetDeviceAuthorizatonState can use it to create a new Access Token.
CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error
// DenyDeviceAuthorization marks a device authorization entry as Denied.
DenyDeviceAuthorization(ctx context.Context, userCode string) error
} }
type deviceLogin struct { type deviceLogin struct {
@ -36,14 +23,14 @@ type deviceLogin struct {
cookie *securecookie.SecureCookie cookie *securecookie.SecureCookie
} }
func registerDeviceAuth(storage deviceAuthenticate, router chi.Router) { func registerDeviceAuth(storage deviceAuthenticate, router *mux.Router) {
l := &deviceLogin{ l := &deviceLogin{
storage: storage, storage: storage,
cookie: securecookie.New(securecookie.GenerateRandomKey(32), nil), cookie: securecookie.New(securecookie.GenerateRandomKey(32), nil),
} }
router.HandleFunc("/", l.userCodeHandler) router.HandleFunc("", l.userCodeHandler)
router.Post("/login", l.loginHandler) router.Path("/login").Methods(http.MethodPost).HandlerFunc(l.loginHandler)
router.HandleFunc("/confirm", l.confirmHandler) router.HandleFunc("/confirm", l.confirmHandler)
} }

View file

@ -5,29 +5,28 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/gorilla/mux"
"github.com/go-chi/chi/v5"
) )
type login struct { type login struct {
authenticate authenticate authenticate authenticate
router chi.Router router *mux.Router
callback func(context.Context, string) string callback func(context.Context, string) string
} }
func NewLogin(authenticate authenticate, callback func(context.Context, string) string, issuerInterceptor *op.IssuerInterceptor) *login { func NewLogin(authenticate authenticate, callback func(context.Context, string) string) *login {
l := &login{ l := &login{
authenticate: authenticate, authenticate: authenticate,
callback: callback, callback: callback,
} }
l.createRouter(issuerInterceptor) l.createRouter()
return l return l
} }
func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) { func (l *login) createRouter() {
l.router = chi.NewRouter() l.router = mux.NewRouter()
l.router.Get("/username", l.loginHandler) l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler)
l.router.Post("/username", issuerInterceptor.HandlerFunc(l.checkLoginHandler)) l.router.Path("/username").Methods("POST").HandlerFunc(l.checkLoginHandler)
} }
type authenticate interface { type authenticate interface {

View file

@ -3,83 +3,75 @@ package exampleop
import ( import (
"crypto/sha256" "crypto/sha256"
"log" "log"
"log/slog"
"net/http" "net/http"
"sync/atomic"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/gorilla/mux"
"github.com/zitadel/logging"
"golang.org/x/text/language" "golang.org/x/text/language"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/zitadel/oidc/v2/example/server/storage"
"github.com/zitadel/oidc/v2/pkg/op"
) )
const ( const (
pathLoggedOut = "/logged-out" pathLoggedOut = "/logged-out"
) )
func init() {
storage.RegisterClients(
storage.NativeClient("native"),
storage.WebClient("web", "secret"),
storage.WebClient("api", "secret"),
)
}
type Storage interface { type Storage interface {
op.Storage op.Storage
authenticate authenticate
deviceAuthenticate deviceAuthenticate
} }
// simple counter for request IDs
var counter atomic.Int64
// SetupServer creates an OIDC server with Issuer=http://localhost:<port> // SetupServer creates an OIDC server with Issuer=http://localhost:<port>
// //
// Use one of the pre-made clients in storage/clients.go or register a new one. // Use one of the pre-made clients in storage/clients.go or register a new one.
func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer bool, extraOptions ...op.Option) chi.Router { func SetupServer(issuer string, storage Storage) *mux.Router {
// the OpenID Provider requires a 32-byte key for (token) encryption // the OpenID Provider requires a 32-byte key for (token) encryption
// be sure to create a proper crypto random key and manage it securely! // be sure to create a proper crypto random key and manage it securely!
key := sha256.Sum256([]byte("test")) key := sha256.Sum256([]byte("test"))
router := chi.NewRouter() router := mux.NewRouter()
router.Use(logging.Middleware(
logging.WithLogger(logger),
logging.WithIDFunc(func() slog.Attr {
return slog.Int64("id", counter.Add(1))
}),
))
// for simplicity, we provide a very small default page for users who have signed out // for simplicity, we provide a very small default page for users who have signed out
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("signed out successfully")) _, err := w.Write([]byte("signed out successfully"))
// no need to check/log error, this will be handled by the middleware. if err != nil {
log.Printf("error serving logged out page: %v", err)
}
}) })
// creation of the OpenIDProvider with the just created in-memory Storage // creation of the OpenIDProvider with the just created in-memory Storage
provider, err := newOP(storage, issuer, key, logger, extraOptions...) provider, err := newOP(storage, issuer, key)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
//the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process // the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process
//for the simplicity of the example this means a simple page with username and password field // for the simplicity of the example this means a simple page with username and password field
//be sure to provide an IssuerInterceptor with the IssuerFromRequest from the OP so the login can select / and pass it to the storage l := NewLogin(storage, op.AuthCallbackURL(provider))
l := NewLogin(storage, op.AuthCallbackURL(provider), op.NewIssuerInterceptor(provider.IssuerFromRequest))
// regardless of how many pages / steps there are in the process, the UI must be registered in the router, // regardless of how many pages / steps there are in the process, the UI must be registered in the router,
// so we will direct all calls to /login to the login UI // so we will direct all calls to /login to the login UI
router.Mount("/login/", http.StripPrefix("/login", l.router)) router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router))
router.Route("/device", func(r chi.Router) { router.PathPrefix("/device").Subrouter()
registerDeviceAuth(storage, r) registerDeviceAuth(storage, router.PathPrefix("/device").Subrouter())
})
handler := http.Handler(provider)
if wrapServer {
handler = op.RegisterLegacyServer(op.NewLegacyServer(provider, *op.DefaultEndpoints), op.AuthorizeCallbackHandler(provider))
}
// we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) // we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
// is served on the correct path // is served on the correct path
// //
// if your issuer ends with a path (e.g. http://localhost:9998/custom/path/), // if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
// then you would have to set the path prefix (/custom/path/) // then you would have to set the path prefix (/custom/path/)
router.Mount("/", handler) router.PathPrefix("/").Handler(provider.HttpHandler())
return router return router
} }
@ -87,7 +79,7 @@ func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer
// newOP will create an OpenID Provider for localhost on a specified port with a given encryption key // newOP will create an OpenID Provider for localhost on a specified port with a given encryption key
// and a predefined default logout uri // and a predefined default logout uri
// it will enable all options (see descriptions) // it will enable all options (see descriptions)
func newOP(storage op.Storage, issuer string, key [32]byte, logger *slog.Logger, extraOptions ...op.Option) (op.OpenIDProvider, error) { func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) {
config := &op.Config{ config := &op.Config{
CryptoKey: key, CryptoKey: key,
@ -115,19 +107,15 @@ func newOP(storage op.Storage, issuer string, key [32]byte, logger *slog.Logger,
DeviceAuthorization: op.DeviceAuthorizationConfig{ DeviceAuthorization: op.DeviceAuthorizationConfig{
Lifetime: 5 * time.Minute, Lifetime: 5 * time.Minute,
PollInterval: 5 * time.Second, PollInterval: 5 * time.Second,
UserFormPath: "/device", UserFormURL: issuer + "device",
UserCode: op.UserCodeBase20, UserCode: op.UserCodeBase20,
}, },
} }
handler, err := op.NewOpenIDProvider(issuer, config, storage, handler, err := op.NewOpenIDProvider(issuer, config, storage,
append([]op.Option{ //we must explicitly allow the use of the http issuer
//we must explicitly allow the use of the http issuer op.WithAllowInsecure(),
op.WithAllowInsecure(), // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
// as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
// Pass our logger to the OP
op.WithLogger(logger.WithGroup("op")),
}, extraOptions...)...,
) )
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -2,58 +2,34 @@ package main
import ( import (
"fmt" "fmt"
"log/slog" "log"
"net/http" "net/http"
"os"
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/config" "github.com/zitadel/oidc/v2/example/server/exampleop"
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/exampleop" "github.com/zitadel/oidc/v2/example/server/storage"
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
) )
func getUserStore(cfg *config.Config) (storage.UserStore, error) {
if cfg.UsersFile == "" {
return storage.NewUserStore(fmt.Sprintf("http://localhost:%s/", cfg.Port)), nil
}
return storage.StoreFromFile(cfg.UsersFile)
}
func main() { func main() {
cfg := config.FromEnvVars(&config.Config{Port: "9998"}) //we will run on :9998
logger := slog.New( port := "9998"
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}),
)
//which gives us the issuer: http://localhost:9998/ //which gives us the issuer: http://localhost:9998/
issuer := fmt.Sprintf("http://localhost:%s/", cfg.Port) issuer := fmt.Sprintf("http://localhost:%s/", port)
storage.RegisterClients(
storage.NativeClient("native", cfg.RedirectURI...),
storage.WebClient("web", "secret", cfg.RedirectURI...),
storage.WebClient("api", "secret", cfg.RedirectURI...),
)
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations // the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
// this might be the layer for accessing your database // this might be the layer for accessing your database
// in this example it will be handled in-memory // in this example it will be handled in-memory
store, err := getUserStore(cfg) storage := storage.NewStorage(storage.NewUserStore(issuer))
if err != nil {
logger.Error("cannot create UserStore", "error", err) router := exampleop.SetupServer(issuer, storage)
os.Exit(1)
}
storage := storage.NewStorage(store)
router := exampleop.SetupServer(issuer, storage, logger, false)
server := &http.Server{ server := &http.Server{
Addr: ":" + cfg.Port, Addr: ":" + port,
Handler: router, Handler: router,
} }
logger.Info("server listening, press ctrl+c to stop", "addr", issuer) log.Printf("server listening on http://localhost:%s/", port)
if server.ListenAndServe() != http.ErrServerClosed { log.Println("press ctrl+c to stop")
logger.Error("server terminated", "error", err) err := server.ListenAndServe()
os.Exit(1) if err != nil {
log.Fatal(err)
} }
} }

View file

@ -3,8 +3,8 @@ package storage
import ( import (
"time" "time"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
var ( var (
@ -32,8 +32,6 @@ type Client struct {
devMode bool devMode bool
idTokenUserinfoClaimsAssertion bool idTokenUserinfoClaimsAssertion bool
clockSkew time.Duration clockSkew time.Duration
postLogoutRedirectURIGlobs []string
redirectURIGlobs []string
} }
// GetID must return the client_id // GetID must return the client_id
@ -184,52 +182,11 @@ func WebClient(id, secret string, redirectURIs ...string) *Client {
applicationType: op.ApplicationTypeWeb, applicationType: op.ApplicationTypeWeb,
authMethod: oidc.AuthMethodBasic, authMethod: oidc.AuthMethodBasic,
loginURL: defaultLoginURL, loginURL: defaultLoginURL,
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode, oidc.ResponseTypeIDTokenOnly, oidc.ResponseTypeIDToken},
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken, oidc.GrantTypeTokenExchange},
accessTokenType: op.AccessTokenTypeBearer,
devMode: true,
idTokenUserinfoClaimsAssertion: false,
clockSkew: 0,
}
}
// DeviceClient creates a device client with Basic authentication.
func DeviceClient(id, secret string) *Client {
return &Client{
id: id,
secret: secret,
redirectURIs: nil,
applicationType: op.ApplicationTypeWeb,
authMethod: oidc.AuthMethodBasic,
loginURL: defaultLoginURL,
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
grantTypes: []oidc.GrantType{oidc.GrantTypeDeviceCode}, grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
accessTokenType: op.AccessTokenTypeBearer, accessTokenType: op.AccessTokenTypeBearer,
devMode: false, devMode: false,
idTokenUserinfoClaimsAssertion: false, idTokenUserinfoClaimsAssertion: false,
clockSkew: 0, clockSkew: 0,
} }
} }
type hasRedirectGlobs struct {
*Client
}
// RedirectURIGlobs provide wildcarding for additional valid redirects
func (c hasRedirectGlobs) RedirectURIGlobs() []string {
return c.redirectURIGlobs
}
// PostLogoutRedirectURIGlobs provide extra wildcarding for additional valid redirects
func (c hasRedirectGlobs) PostLogoutRedirectURIGlobs() []string {
return c.postLogoutRedirectURIGlobs
}
// RedirectGlobsClient wraps the client in a op.HasRedirectGlobs
// only if DevMode is enabled.
func RedirectGlobsClient(client *Client) op.Client {
if client.devMode {
return hasRedirectGlobs{client}
}
return client
}

View file

@ -1,13 +1,12 @@
package storage package storage
import ( import (
"log/slog"
"time" "time"
"golang.org/x/text/language" "golang.org/x/text/language"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
const ( const (
@ -35,25 +34,11 @@ type AuthRequest struct {
UserID string UserID string
Scopes []string Scopes []string
ResponseType oidc.ResponseType ResponseType oidc.ResponseType
ResponseMode oidc.ResponseMode
Nonce string Nonce string
CodeChallenge *OIDCCodeChallenge CodeChallenge *OIDCCodeChallenge
done bool passwordChecked bool
authTime time.Time authTime time.Time
}
// LogValue allows you to define which fields will be logged.
// Implements the [slog.LogValuer]
func (a *AuthRequest) LogValue() slog.Value {
return slog.GroupValue(
slog.String("id", a.ID),
slog.Time("creation_date", a.CreationDate),
slog.Any("scopes", a.Scopes),
slog.String("response_type", string(a.ResponseType)),
slog.String("app_id", a.ApplicationID),
slog.String("callback_uri", a.CallbackURI),
)
} }
func (a *AuthRequest) GetID() string { func (a *AuthRequest) GetID() string {
@ -66,7 +51,7 @@ func (a *AuthRequest) GetACR() string {
func (a *AuthRequest) GetAMR() []string { func (a *AuthRequest) GetAMR() []string {
// this example only uses password for authentication // this example only uses password for authentication
if a.done { if a.passwordChecked {
return []string{"pwd"} return []string{"pwd"}
} }
return nil return nil
@ -101,7 +86,7 @@ func (a *AuthRequest) GetResponseType() oidc.ResponseType {
} }
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
return a.ResponseMode return "" // we won't handle response mode in this example
} }
func (a *AuthRequest) GetScopes() []string { func (a *AuthRequest) GetScopes() []string {
@ -117,11 +102,11 @@ func (a *AuthRequest) GetSubject() string {
} }
func (a *AuthRequest) Done() bool { func (a *AuthRequest) Done() bool {
return a.done return a.passwordChecked // this example only uses password for authentication
} }
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string { func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
prompts := make([]string, 0, len(oidcPrompt)) prompts := make([]string, len(oidcPrompt))
for _, oidcPrompt := range oidcPrompt { for _, oidcPrompt := range oidcPrompt {
switch oidcPrompt { switch oidcPrompt {
case oidc.PromptNone, case oidc.PromptNone,
@ -155,7 +140,6 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques
UserID: userID, UserID: userID,
Scopes: authReq.Scopes, Scopes: authReq.Scopes,
ResponseType: authReq.ResponseType, ResponseType: authReq.ResponseType,
ResponseMode: authReq.ResponseMode,
Nonce: authReq.Nonce, Nonce: authReq.Nonce,
CodeChallenge: &OIDCCodeChallenge{ CodeChallenge: &OIDCCodeChallenge{
Challenge: authReq.CodeChallenge, Challenge: authReq.CodeChallenge,
@ -164,15 +148,6 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques
} }
} }
type AuthRequestWithSessionState struct {
*AuthRequest
SessionState string
}
func (a *AuthRequestWithSessionState) GetSessionState() string {
return a.SessionState
}
type OIDCCodeChallenge struct { type OIDCCodeChallenge struct {
Challenge string Challenge string
Method string Method string

View file

@ -11,11 +11,11 @@ import (
"sync" "sync"
"time" "time"
jose "github.com/go-jose/go-jose/v4"
"github.com/google/uuid" "github.com/google/uuid"
"gopkg.in/square/go-jose.v2"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant // serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
@ -28,10 +28,8 @@ var serviceKey1 = &rsa.PublicKey{
E: 65537, E: 65537,
} }
var ( // var _ op.Storage = &storage{}
_ op.Storage = &Storage{} // var _ op.ClientCredentialsStorage = &storage{}
_ op.ClientCredentialsStorage = &Storage{}
)
// storage implements the op.Storage interface // storage implements the op.Storage interface
// typically you would implement this as a layer on top of your database // typically you would implement this as a layer on top of your database
@ -48,7 +46,6 @@ type Storage struct {
signingKey signingKey signingKey signingKey
deviceCodes map[string]deviceAuthorizationEntry deviceCodes map[string]deviceAuthorizationEntry
userCodes map[string]string userCodes map[string]string
serviceUsers map[string]*Client
} }
type signingKey struct { type signingKey struct {
@ -61,7 +58,7 @@ func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm {
return s.algorithm return s.algorithm
} }
func (s *signingKey) Key() any { func (s *signingKey) Key() interface{} {
return s.key return s.key
} }
@ -85,15 +82,11 @@ func (s *publicKey) Use() string {
return "sig" return "sig"
} }
func (s *publicKey) Key() any { func (s *publicKey) Key() interface{} {
return &s.key.PublicKey return &s.key.PublicKey
} }
func NewStorage(userStore UserStore) *Storage { func NewStorage(userStore UserStore) *Storage {
return NewStorageWithClients(userStore, clients)
}
func NewStorageWithClients(userStore UserStore, clients map[string]*Client) *Storage {
key, _ := rsa.GenerateKey(rand.Reader, 2048) key, _ := rsa.GenerateKey(rand.Reader, 2048)
return &Storage{ return &Storage{
authRequests: make(map[string]*AuthRequest), authRequests: make(map[string]*AuthRequest),
@ -116,16 +109,6 @@ func NewStorageWithClients(userStore UserStore, clients map[string]*Client) *Sto
}, },
deviceCodes: make(map[string]deviceAuthorizationEntry), deviceCodes: make(map[string]deviceAuthorizationEntry),
userCodes: make(map[string]string), userCodes: make(map[string]string),
serviceUsers: map[string]*Client{
"sid1": {
id: "sid1",
secret: "verysecret",
grantTypes: []oidc.GrantType{
oidc.GrantTypeClientCredentials,
},
accessTokenType: op.AccessTokenTypeBearer,
},
},
} }
} }
@ -150,10 +133,7 @@ func (s *Storage) CheckUsernamePassword(username, password, id string) error {
// you will have to change some state on the request to guide the user through possible multiple steps of the login process // you will have to change some state on the request to guide the user through possible multiple steps of the login process
// in this example we'll simply check the username / password and set a boolean to true // in this example we'll simply check the username / password and set a boolean to true
// therefore we will also just check this boolean if the request / login has been finished // therefore we will also just check this boolean if the request / login has been finished
request.done = true request.passwordChecked = true
request.authTime = time.Now()
return nil return nil
} }
return fmt.Errorf("username or password wrong") return fmt.Errorf("username or password wrong")
@ -176,12 +156,6 @@ func (s *Storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthReque
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
if len(authReq.Prompt) == 1 && authReq.Prompt[0] == "none" {
// With prompt=none, there is no way for the user to log in
// so return error right away.
return nil, oidc.ErrLoginRequired()
}
// typically, you'll fill your storage / storage model with the information of the passed object // typically, you'll fill your storage / storage model with the information of the passed object
request := authRequestToInternal(authReq, userID) request := authRequestToInternal(authReq, userID)
@ -298,19 +272,15 @@ func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.T
// if we get here, the currentRefreshToken was not empty, so the call is a refresh token request // if we get here, the currentRefreshToken was not empty, so the call is a refresh token request
// we therefore will have to check the currentRefreshToken and renew the refresh token // we therefore will have to check the currentRefreshToken and renew the refresh token
refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken)
newRefreshToken = uuid.NewString()
accessToken, err := s.accessToken(applicationID, newRefreshToken, request.GetSubject(), request.GetAudience(), request.GetScopes())
if err != nil { if err != nil {
return "", "", time.Time{}, err return "", "", time.Time{}, err
} }
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
if err := s.renewRefreshToken(currentRefreshToken, newRefreshToken, accessToken.ID); err != nil { if err != nil {
return "", "", time.Time{}, err return "", "", time.Time{}, err
} }
return accessToken.ID, refreshToken, accessToken.Expiration, nil
return accessToken.ID, newRefreshToken, accessToken.Expiration, nil
} }
func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
@ -392,9 +362,14 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID
if refreshToken.ApplicationID != clientID { if refreshToken.ApplicationID != clientID {
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
} }
delete(s.refreshTokens, refreshToken.ID)
// if it is a refresh token, you will have to remove the access token as well // if it is a refresh token, you will have to remove the access token as well
delete(s.tokens, refreshToken.AccessToken) delete(s.refreshTokens, refreshToken.ID)
for _, accessToken := range s.tokens {
if accessToken.RefreshTokenID == refreshToken.ID {
delete(s.tokens, accessToken.ID)
return nil
}
}
return nil return nil
} }
@ -432,7 +407,7 @@ func (s *Storage) GetClientByClientID(ctx context.Context, clientID string) (op.
if !ok { if !ok {
return nil, fmt.Errorf("client not found") return nil, fmt.Errorf("client not found")
} }
return RedirectGlobsClient(client), nil return client, nil
} }
// AuthorizeClientIDSecret implements the op.Storage interface // AuthorizeClientIDSecret implements the op.Storage interface
@ -452,22 +427,15 @@ func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientS
return nil return nil
} }
// SetUserinfoFromScopes implements the op.Storage interface. // SetUserinfoFromScopes implements the op.Storage interface
// Provide an empty implementation and use SetUserinfoFromRequest instead. // it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error { func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error {
return nil return s.setUserinfo(ctx, userinfo, userID, clientID, scopes)
}
// SetUserinfoFromRequests implements the op.CanSetUserinfoFromRequest interface. In the
// next major release, it will be required for op.Storage.
// It will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
func (s *Storage) SetUserinfoFromRequest(ctx context.Context, userinfo *oidc.UserInfo, token op.IDTokenRequest, scopes []string) error {
return s.setUserinfo(ctx, userinfo, token.GetSubject(), token.GetClientID(), scopes)
} }
// SetUserinfoFromToken implements the op.Storage interface // SetUserinfoFromToken implements the op.Storage interface
// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function // it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function
func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error { func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error {
token, ok := func() (*Token, bool) { token, ok := func() (*Token, bool) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -490,15 +458,12 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserI
// return err // return err
// } // }
//} //}
if token.Expiration.Before(time.Now()) {
return fmt.Errorf("token is expired")
}
return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes) return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes)
} }
// SetIntrospectionFromToken implements the op.Storage interface // SetIntrospectionFromToken implements the op.Storage interface
// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function // it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function
func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error { func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
token, ok := func() (*Token, bool) { token, ok := func() (*Token, bool) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@ -515,17 +480,14 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *
// this will automatically be done by the library if you don't return an error // this will automatically be done by the library if you don't return an error
// you can also return further information about the user / associated token // you can also return further information about the user / associated token
// e.g. the userinfo (equivalent to userinfo endpoint) // e.g. the userinfo (equivalent to userinfo endpoint)
err := s.setUserinfo(ctx, introspection, subject, clientID, token.Scopes)
userInfo := new(oidc.UserInfo)
err := s.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes)
if err != nil { if err != nil {
return err return err
} }
introspection.SetUserInfo(userInfo)
//...and also the requested scopes... //...and also the requested scopes...
introspection.Scope = token.Scopes introspection.SetScopes(token.Scopes)
//...and the client the token was issued to //...and the client the token was issued to
introspection.ClientID = token.ApplicationID introspection.SetClientID(token.ApplicationID)
return nil return nil
} }
} }
@ -534,11 +496,11 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *
// GetPrivateClaimsFromScopes implements the op.Storage interface // GetPrivateClaimsFromScopes implements the op.Storage interface
// it will be called for the creation of a JWT access token to assert claims for custom scopes // it will be called for the creation of a JWT access token to assert claims for custom scopes
func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]any, err error) { func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
return s.getPrivateClaimsFromScopes(ctx, userID, clientID, scopes) return s.getPrivateClaimsFromScopes(ctx, userID, clientID, scopes)
} }
func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]any, err error) { func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
for _, scope := range scopes { for _, scope := range scopes {
switch scope { switch scope {
case CustomScope: case CustomScope:
@ -599,41 +561,33 @@ func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime
Audience: accessToken.Audience, Audience: accessToken.Audience,
Expiration: time.Now().Add(5 * time.Hour), Expiration: time.Now().Add(5 * time.Hour),
Scopes: accessToken.Scopes, Scopes: accessToken.Scopes,
AccessToken: accessToken.ID,
} }
s.refreshTokens[token.ID] = token s.refreshTokens[token.ID] = token
return token.Token, nil return token.Token, nil
} }
// renewRefreshToken checks the provided refresh_token and creates a new one based on the current // renewRefreshToken checks the provided refresh_token and creates a new one based on the current
// func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) {
// [Refresh Token Rotation] is implemented.
//
// [Refresh Token Rotation]: https://www.rfc-editor.org/rfc/rfc6819#section-5.2.2.3
func (s *Storage) renewRefreshToken(currentRefreshToken, newRefreshToken, newAccessToken string) error {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
refreshToken, ok := s.refreshTokens[currentRefreshToken] refreshToken, ok := s.refreshTokens[currentRefreshToken]
if !ok { if !ok {
return fmt.Errorf("invalid refresh token") return "", "", fmt.Errorf("invalid refresh token")
} }
// deletes the refresh token // deletes the refresh token and all access tokens which were issued based on this refresh token
delete(s.refreshTokens, currentRefreshToken) delete(s.refreshTokens, currentRefreshToken)
for _, token := range s.tokens {
// delete the access token which was issued based on this refresh token if token.RefreshTokenID == currentRefreshToken {
delete(s.tokens, refreshToken.AccessToken) delete(s.tokens, token.ID)
break
if refreshToken.Expiration.Before(time.Now()) { }
return fmt.Errorf("expired refresh token")
} }
// creates a new refresh token based on the current one // creates a new refresh token based on the current one
refreshToken.Token = newRefreshToken token := uuid.NewString()
refreshToken.ID = newRefreshToken refreshToken.Token = token
refreshToken.Expiration = time.Now().Add(5 * time.Hour) refreshToken.ID = token
refreshToken.AccessToken = newAccessToken s.refreshTokens[token] = refreshToken
s.refreshTokens[newRefreshToken] = refreshToken return token, refreshToken.ID, nil
return nil
} }
// accessToken will store an access_token in-memory based on the provided information // accessToken will store an access_token in-memory based on the provided information
@ -654,7 +608,7 @@ func (s *Storage) accessToken(applicationID, refreshTokenID, subject string, aud
} }
// setUserinfo sets the info based on the user, scopes and if necessary the clientID // setUserinfo sets the info based on the user, scopes and if necessary the clientID
func (s *Storage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, userID, clientID string, scopes []string) (err error) { func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, clientID string, scopes []string) (err error) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
user := s.userStore.GetUserByID(userID) user := s.userStore.GetUserByID(userID)
@ -664,19 +618,17 @@ func (s *Storage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, user
for _, scope := range scopes { for _, scope := range scopes {
switch scope { switch scope {
case oidc.ScopeOpenID: case oidc.ScopeOpenID:
userInfo.Subject = user.ID userInfo.SetSubject(user.ID)
case oidc.ScopeEmail: case oidc.ScopeEmail:
userInfo.Email = user.Email userInfo.SetEmail(user.Email, user.EmailVerified)
userInfo.EmailVerified = oidc.Bool(user.EmailVerified)
case oidc.ScopeProfile: case oidc.ScopeProfile:
userInfo.PreferredUsername = user.Username userInfo.SetPreferredUsername(user.Username)
userInfo.Name = user.FirstName + " " + user.LastName userInfo.SetName(user.FirstName + " " + user.LastName)
userInfo.FamilyName = user.LastName userInfo.SetFamilyName(user.LastName)
userInfo.GivenName = user.FirstName userInfo.SetGivenName(user.FirstName)
userInfo.Locale = oidc.NewLocale(user.PreferredLanguage) userInfo.SetLocale(user.PreferredLanguage)
case oidc.ScopePhone: case oidc.ScopePhone:
userInfo.PhoneNumber = user.Phone userInfo.SetPhone(user.Phone, user.PhoneVerified)
userInfo.PhoneNumberVerified = user.PhoneVerified
case CustomScope: case CustomScope:
// you can also have a custom scope and assert public or custom claims based on that // you can also have a custom scope and assert public or custom claims based on that
userInfo.AppendClaims(CustomClaim, customClaim(clientID)) userInfo.AppendClaims(CustomClaim, customClaim(clientID))
@ -730,7 +682,7 @@ func (s *Storage) CreateTokenExchangeRequest(ctx context.Context, request op.Tok
// GetPrivateClaimsFromScopesForTokenExchange implements the op.TokenExchangeStorage interface // GetPrivateClaimsFromScopesForTokenExchange implements the op.TokenExchangeStorage interface
// it will be called for the creation of an exchanged JWT access token to assert claims for custom scopes // it will be called for the creation of an exchanged JWT access token to assert claims for custom scopes
// plus adding token exchange specific claims related to delegation or impersonation // plus adding token exchange specific claims related to delegation or impersonation
func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]any, err error) { func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}, err error) {
claims, err = s.getPrivateClaimsFromScopes(ctx, "", request.GetClientID(), request.GetScopes()) claims, err = s.getPrivateClaimsFromScopes(ctx, "", request.GetClientID(), request.GetScopes())
if err != nil { if err != nil {
return nil, err return nil, err
@ -746,7 +698,7 @@ func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context,
// SetUserinfoFromScopesForTokenExchange implements the op.TokenExchangeStorage interface // SetUserinfoFromScopesForTokenExchange implements the op.TokenExchangeStorage interface
// it will be called for the creation of an id_token - we are using the same private function as for other flows, // it will be called for the creation of an id_token - we are using the same private function as for other flows,
// plus adding token exchange specific claims related to delegation or impersonation // plus adding token exchange specific claims related to delegation or impersonation
func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo *oidc.UserInfo, request op.TokenExchangeRequest) error { func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo oidc.UserInfoSetter, request op.TokenExchangeRequest) error {
err := s.setUserinfo(ctx, userinfo, request.GetSubject(), request.GetClientID(), request.GetScopes()) err := s.setUserinfo(ctx, userinfo, request.GetSubject(), request.GetClientID(), request.GetScopes())
if err != nil { if err != nil {
return err return err
@ -759,12 +711,12 @@ func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, useri
return nil return nil
} }
func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]any) { func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}) {
for _, scope := range request.GetScopes() { for _, scope := range request.GetScopes() {
switch { switch {
case strings.HasPrefix(scope, CustomScopeImpersonatePrefix) && request.GetExchangeActor() == "": case strings.HasPrefix(scope, CustomScopeImpersonatePrefix) && request.GetExchangeActor() == "":
// Set actor subject claim for impersonation flow // Set actor subject claim for impersonation flow
claims = appendClaim(claims, "act", map[string]any{ claims = appendClaim(claims, "act", map[string]interface{}{
"sub": request.GetExchangeSubject(), "sub": request.GetExchangeSubject(),
}) })
} }
@ -772,7 +724,7 @@ func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenEx
// Set actor subject claim for delegation flow // Set actor subject claim for delegation flow
// if request.GetExchangeActor() != "" { // if request.GetExchangeActor() != "" {
// claims = appendClaim(claims, "act", map[string]any{ // claims = appendClaim(claims, "act", map[string]interface{}{
// "sub": request.GetExchangeActor(), // "sub": request.GetExchangeActor(),
// }) // })
// } // }
@ -794,16 +746,16 @@ func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Tim
} }
// customClaim demonstrates how to return custom claims based on provided information // customClaim demonstrates how to return custom claims based on provided information
func customClaim(clientID string) map[string]any { func customClaim(clientID string) map[string]interface{} {
return map[string]any{ return map[string]interface{}{
"client": clientID, "client": clientID,
"other": "stuff", "other": "stuff",
} }
} }
func appendClaim(claims map[string]any, claim string, value any) map[string]any { func appendClaim(claims map[string]interface{}, claim string, value interface{}) map[string]interface{} {
if claims == nil { if claims == nil {
claims = make(map[string]any) claims = make(map[string]interface{})
} }
claims[claim] = value claims[claim] = value
return claims return claims
@ -890,44 +842,3 @@ func (s *Storage) DenyDeviceAuthorization(ctx context.Context, userCode string)
s.deviceCodes[s.userCodes[userCode]].state.Denied = true s.deviceCodes[s.userCodes[userCode]].state.Denied = true
return nil return nil
} }
// AuthRequestDone is used by testing and is not required to implement op.Storage
func (s *Storage) AuthRequestDone(id string) error {
s.lock.Lock()
defer s.lock.Unlock()
if req, ok := s.authRequests[id]; ok {
req.done = true
return nil
}
return errors.New("request not found")
}
func (s *Storage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) {
s.lock.Lock()
defer s.lock.Unlock()
client, ok := s.serviceUsers[clientID]
if !ok {
return nil, errors.New("wrong service user or password")
}
if client.secret != clientSecret {
return nil, errors.New("wrong service user or password")
}
return client, nil
}
func (s *Storage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (op.TokenRequest, error) {
client, ok := s.serviceUsers[clientID]
if !ok {
return nil, errors.New("wrong service user or password")
}
return &oidc.JWTTokenRequest{
Subject: client.id,
Audience: []string{clientID},
Scopes: scopes,
}, nil
}

View file

@ -4,10 +4,10 @@ import (
"context" "context"
"time" "time"
jose "github.com/go-jose/go-jose/v4" "gopkg.in/square/go-jose.v2"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
type multiStorage struct { type multiStorage struct {
@ -196,9 +196,9 @@ func (s *multiStorage) AuthorizeClientIDSecret(ctx context.Context, clientID, cl
return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret)
} }
// SetUserinfoFromScopes implements the op.Storage interface. // SetUserinfoFromScopes implements the op.Storage interface
// Provide an empty implementation and use SetUserinfoFromRequest instead. // it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error { func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error {
storage, err := s.storageFromContext(ctx) storage, err := s.storageFromContext(ctx)
if err != nil { if err != nil {
return err return err
@ -206,20 +206,9 @@ func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc
return storage.SetUserinfoFromScopes(ctx, userinfo, userID, clientID, scopes) return storage.SetUserinfoFromScopes(ctx, userinfo, userID, clientID, scopes)
} }
// SetUserinfoFromRequests implements the op.CanSetUserinfoFromRequest interface. In the
// next major release, it will be required for op.Storage.
// It will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
func (s *multiStorage) SetUserinfoFromRequest(ctx context.Context, userinfo *oidc.UserInfo, token op.IDTokenRequest, scopes []string) error {
storage, err := s.storageFromContext(ctx)
if err != nil {
return err
}
return storage.SetUserinfoFromRequest(ctx, userinfo, token, scopes)
}
// SetUserinfoFromToken implements the op.Storage interface // SetUserinfoFromToken implements the op.Storage interface
// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function // it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function
func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error { func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error {
storage, err := s.storageFromContext(ctx) storage, err := s.storageFromContext(ctx)
if err != nil { if err != nil {
return err return err
@ -229,7 +218,7 @@ func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.
// SetIntrospectionFromToken implements the op.Storage interface // SetIntrospectionFromToken implements the op.Storage interface
// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function // it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function
func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error { func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
storage, err := s.storageFromContext(ctx) storage, err := s.storageFromContext(ctx)
if err != nil { if err != nil {
return err return err
@ -239,7 +228,7 @@ func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspect
// GetPrivateClaimsFromScopes implements the op.Storage interface // GetPrivateClaimsFromScopes implements the op.Storage interface
// it will be called for the creation of a JWT access token to assert claims for custom scopes // it will be called for the creation of a JWT access token to assert claims for custom scopes
func (s *multiStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]any, err error) { func (s *multiStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
storage, err := s.storageFromContext(ctx) storage, err := s.storageFromContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -22,5 +22,4 @@ type RefreshToken struct {
ApplicationID string ApplicationID string
Expiration time.Time Expiration time.Time
Scopes []string Scopes []string
AccessToken string // Token.ID
} }

View file

@ -2,8 +2,6 @@ package storage
import ( import (
"crypto/rsa" "crypto/rsa"
"encoding/json"
"os"
"strings" "strings"
"golang.org/x/text/language" "golang.org/x/text/language"
@ -37,18 +35,6 @@ type userStore struct {
users map[string]*User users map[string]*User
} }
func StoreFromFile(path string) (UserStore, error) {
users := map[string]*User{}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if err := json.Unmarshal(data, &users); err != nil {
return nil, err
}
return userStore{users}, nil
}
func NewUserStore(issuer string) UserStore { func NewUserStore(issuer string) UserStore {
hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0] hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
return userStore{ return userStore{

View file

@ -1,70 +0,0 @@
package storage
import (
"os"
"path"
"reflect"
"testing"
"golang.org/x/text/language"
)
func TestStoreFromFile(t *testing.T) {
for _, tc := range []struct {
name string
pathToFile string
content string
want UserStore
wantErr bool
}{
{
name: "normal user file",
pathToFile: "userfile.json",
content: `{
"id1": {
"ID": "id1",
"EmailVerified": true,
"PreferredLanguage": "DE"
}
}`,
want: userStore{map[string]*User{
"id1": {
ID: "id1",
EmailVerified: true,
PreferredLanguage: language.German,
},
}},
},
{
name: "malformed file",
pathToFile: "whatever",
content: "not a json just a text",
wantErr: true,
},
{
name: "not existing file",
pathToFile: "what/ever/file",
wantErr: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
actualPath := path.Join(t.TempDir(), tc.pathToFile)
if tc.content != "" && tc.pathToFile != "" {
if err := os.WriteFile(actualPath, []byte(tc.content), 0666); err != nil {
t.Fatalf("cannot create file with test content: %q", tc.content)
}
}
result, err := StoreFromFile(actualPath)
if err != nil && !tc.wantErr {
t.Errorf("StoreFromFile(%q) returned unexpected error %q", tc.pathToFile, err)
} else if err == nil && tc.wantErr {
t.Errorf("StoreFromFile(%q) did not return an expected error", tc.pathToFile)
}
if !tc.wantErr && !reflect.DeepEqual(tc.want, result.(userStore)) {
t.Errorf("expected StoreFromFile(%q) = %v, but got %v",
tc.pathToFile, tc.want, result)
}
})
}
}

50
go.mod
View file

@ -1,40 +1,22 @@
module git.christmann.info/LARA/zitadel-oidc/v3 module github.com/zitadel/oidc/v2
go 1.23.7 go 1.16
toolchain go1.24.1
require ( require (
github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/go-chi/chi/v5 v5.2.1
github.com/go-jose/go-jose/v4 v4.0.5
github.com/golang/mock v1.6.0 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/go-github/v31 v31.0.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.3.0
github.com/gorilla/securecookie v1.1.2 github.com/gorilla/mux v1.8.0
github.com/jeremija/gosubmit v0.2.8 github.com/gorilla/schema v1.2.0
github.com/muhlemmer/gu v0.3.1 github.com/gorilla/securecookie v1.1.1
github.com/muhlemmer/httpforwarded v0.1.0 github.com/jeremija/gosubmit v0.2.7
github.com/rs/cors v1.11.1 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/sirupsen/logrus v1.9.3 github.com/rs/cors v1.8.3
github.com/stretchr/testify v1.10.0 github.com/sirupsen/logrus v1.9.0
github.com/zitadel/logging v0.6.2 github.com/stretchr/testify v1.8.1
github.com/zitadel/schema v1.3.1 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
go.opentelemetry.io/otel v1.29.0 golang.org/x/text v0.6.0
golang.org/x/oauth2 v0.30.0 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
golang.org/x/text v0.26.0 gopkg.in/square/go-jose.v2 v2.6.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

439
go.sum
View file

@ -1,108 +1,429 @@
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 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=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= 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/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
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/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
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=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View file

@ -1,58 +0,0 @@
// Package gen allows generating of example tokens and claims.
//
// go run ./internal/testutil/gen
package main
import (
"encoding/json"
"fmt"
"os"
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
)
var custom = map[string]any{
"foo": "Hello, World!",
"bar": struct {
Count int `json:"count,omitempty"`
Tags []string `json:"tags,omitempty"`
}{
Count: 22,
Tags: []string{"some", "tags"},
},
}
func main() {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
accessToken, atClaims := tu.NewAccessTokenCustom(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration.AddDate(99, 0, 0), tu.ValidJWTID,
tu.ValidClientID, tu.ValidSkew, custom,
)
atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm)
if err != nil {
panic(err)
}
idToken, idClaims := tu.NewIDTokenCustom(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration.AddDate(99, 0, 0), tu.ValidAuthTime,
tu.ValidNonce, tu.ValidACR, tu.ValidAMR, tu.ValidClientID,
tu.ValidSkew, atHash, custom,
)
fmt.Println("access token claims:")
if err := enc.Encode(atClaims); err != nil {
panic(err)
}
fmt.Printf("access token:\n%s\n", accessToken)
fmt.Println("ID token claims:")
if err := enc.Encode(idClaims); err != nil {
panic(err)
}
fmt.Printf("ID token:\n%s\n", idToken)
}

View file

@ -1,180 +0,0 @@
// Package testuril helps setting up required data for testing,
// such as tokens, claims and verifiers.
package testutil
import (
"context"
"encoding/json"
"errors"
"time"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
jose "github.com/go-jose/go-jose/v4"
"github.com/muhlemmer/gu"
)
// KeySet implements oidc.Keys
type KeySet struct{}
// VerifySignature implments op.KeySet.
func (KeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
if err = ctx.Err(); err != nil {
return nil, err
}
return jws.Verify(WebKey.Public())
}
// use a reproducible signing key
const webkeyJSON = `{"kty":"RSA","kid":"1","alg":"PS512","n":"x6JoG8t2Li68JSwPwnh51TvHYFf3z72tQ3wmJG3VosU6MdJF0gSTCIwflOJ38OWE6hYtN1WAeyBy2CYdnXd1QZzkK_apGK4M7hsNA9jCTg8NOZjLPL0ww1jp7313Skla7mbm90uNdg4TUNp2n_r-sCYywI-9cfSlhzLSksxKK_BRdzy6xW20daAcI-mErQXIcvdYIguunJk_uTb8kJedsWMcQ4Mb57QujUok2Z2YabWyb9Fi1_StixXJvd_WEu93SHNMORB0u6ymnO3aZJdATLdhtcP-qsVicQhffpqVazmZQPf7K-7n4I5vJE4g9XXzZ2dSKSp3Ewe_nna_2kvbCw","e":"AQAB","d":"sl3F_QeF2O-CxQegMRYpbL6Tfd47GM6VDxXOkn_cACmNvFPudB4ILPvdf830cjTv06Lq1WS8fcZZNgygK0A_cNc3-pvRK67e-KMMtuIlgU7rdwmwlN1Iw1Ee-w6z1ZjC-PzR4iQMCW28DmKS2I-OnV4TvH7xOe7nMmvTPrvujV__YKfUxvAWXJG7_wtaJBGplezn5nNsKG2Ot9h0mhMdYUgGC36wLxo3Q5d4m79EXQYdhm89EfxogwvMmHRes5PNpHRuDZRHGAI4RZi2KvgmqF07e1Qdq4TqbQnY5pCYrdjqvEFFjGC6jTE-ak_b21FcSVy-9aZHyf04U4g5-cIUEQ","p":"7AaicFryJCHRekdSkx8tfPxaSiyEuN8jhP9cLqs4rLkIbrSHmanPhjnLe-Tlh3icQ8hPoy6WC8ktLwsrzbfGIh4U_zgAfvtD1Y_lZM-YSWZsxqlrGiI5do11iVzzoy4a1XdkgOjHQz9y6J-uoA9jY8ILG7VaEZQnaYwWZV3cspk","q":"2Ide9hlwthXJQJYqI0mibM5BiGBxJ4CafPmF1DYNXggBCczZ6ERGReNTGM_AEhy5mvLXUH6uBSOJlfHTYzx49C1GgIO3hEWVEGAKAytVRL6RfAkVSOXMQUp-HjXKpGg_Nx1SJxQf3rulbW8HXO4KqIlloyIXpPQSK7jB8A4hJUM","dp":"1nmc6F4sRNsaQHRJO_mL21RxM4_KtzfFThjCCoJ6iLHHUNnpkp_1PTKNjrLMRFM8JHgErfMqU-FmlqYfEtvZRq1xRQ39nWX0GT-eIwJljuVtGQVglqnc77bRxJXbqz-9EJdik6VzVM92Op7IDxiMp1zvvSkJhInNWqL6wvgNEZk","dq":"dlHizlAwiw90ndpwxD-khhhfLwqkSpW31br0KnYu78cn6hcKrCVC0UXbTp-XsU4JDmbMyauvpBc7Q7iVbpDI94UWFXvkeF8diYkxb3HqclpAXasI-oC4EKWILTHvvc9JW_Clx7zzfV7Ekvws5dcd8-LAq1gh232TwFiBgY_3BMk","qi":"E1k_9W3odXgcmIP2PCJztE7hB7jeuAL1ElAY88VJBBPY670uwOEjKL2VfQuz9q9IjzLAvcgf7vS9blw2RHP_XqHqSOlJWGwvMQTF0Q8zLknCgKt8q7HQQNWIJcBZ8qdUVn02-qf4E3tgZ3JHaHNs8imA_L-__WoUmzC4z5jH_lM"}`
const SignatureAlgorithm = jose.RS256
var (
WebKey jose.JSONWebKey
Signer jose.Signer
)
func init() {
err := json.Unmarshal([]byte(webkeyJSON), &WebKey)
if err != nil {
panic(err)
}
Signer, err = jose.NewSigner(jose.SigningKey{Algorithm: SignatureAlgorithm, Key: WebKey}, nil)
if err != nil {
panic(err)
}
}
type JWTProfileKeyStorage struct{}
func (JWTProfileKeyStorage) GetKeyByIDAndClientID(ctx context.Context, keyID string, clientID string) (*jose.JSONWebKey, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
return gu.Ptr(WebKey.Public()), nil
}
func signEncodeTokenClaims(claims any) string {
payload, err := json.Marshal(claims)
if err != nil {
panic(err)
}
object, err := Signer.Sign(payload)
if err != nil {
panic(err)
}
token, err := object.CompactSerialize()
if err != nil {
panic(err)
}
return token
}
func claimsMap(claims any) map[string]any {
data, err := json.Marshal(claims)
if err != nil {
panic(err)
}
dst := make(map[string]any)
if err = json.Unmarshal(data, &dst); err != nil {
panic(err)
}
return dst
}
func NewIDTokenCustom(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration, atHash string, custom map[string]any) (string, *oidc.IDTokenClaims) {
claims := oidc.NewIDTokenClaims(issuer, subject, audience, expiration, authTime, nonce, acr, amr, clientID, skew)
claims.AccessTokenHash = atHash
claims.Claims = custom
token := signEncodeTokenClaims(claims)
// set this so that assertion in tests will work
claims.SignatureAlg = SignatureAlgorithm
claims.Claims = claimsMap(claims)
return token, claims
}
// NewIDToken creates a new IDTokenClaims with passed data and returns a signed token and claims.
func NewIDToken(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration, atHash string) (string, *oidc.IDTokenClaims) {
return NewIDTokenCustom(issuer, subject, audience, expiration, authTime, nonce, acr, amr, clientID, skew, atHash, nil)
}
func NewAccessTokenCustom(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration, custom map[string]any) (string, *oidc.AccessTokenClaims) {
claims := oidc.NewAccessTokenClaims(issuer, subject, audience, expiration, jwtid, clientID, skew)
claims.Claims = custom
token := signEncodeTokenClaims(claims)
// set this so that assertion in tests will work
claims.SignatureAlg = SignatureAlgorithm
claims.Claims = claimsMap(claims)
return token, claims
}
// NewAcccessToken creates a new AccessTokenClaims with passed data and returns a signed token and claims.
func NewAccessToken(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration) (string, *oidc.AccessTokenClaims) {
return NewAccessTokenCustom(issuer, subject, audience, expiration, jwtid, clientID, skew, nil)
}
func NewJWTProfileAssertion(issuer, clientID string, audience []string, issuedAt, expiration time.Time) (string, *oidc.JWTTokenRequest) {
req := &oidc.JWTTokenRequest{
Issuer: issuer,
Subject: clientID,
Audience: audience,
ExpiresAt: oidc.FromTime(expiration),
IssuedAt: oidc.FromTime(issuedAt),
}
// make sure the private claim map is set correctly
data, err := json.Marshal(req)
if err != nil {
panic(err)
}
if err = json.Unmarshal(data, req); err != nil {
panic(err)
}
return signEncodeTokenClaims(req), req
}
const InvalidSignatureToken = `eyJhbGciOiJQUzUxMiJ9.eyJpc3MiOiJsb2NhbC5jb20iLCJzdWIiOiJ0aW1AbG9jYWwuY29tIiwiYXVkIjpbInVuaXQiLCJ0ZXN0IiwiNTU1NjY2Il0sImV4cCI6MTY3Nzg0MDQzMSwiaWF0IjoxNjc3ODQwMzcwLCJhdXRoX3RpbWUiOjE2Nzc4NDAzMTAsIm5vbmNlIjoiMTIzNDUiLCJhY3IiOiJzb21ldGhpbmciLCJhbXIiOlsiZm9vIiwiYmFyIl0sImF6cCI6IjU1NTY2NiJ9.DtZmvVkuE4Hw48ijBMhRJbxEWCr_WEYuPQBMY73J9TP6MmfeNFkjVJf4nh4omjB9gVLnQ-xhEkNOe62FS5P0BB2VOxPuHZUj34dNspCgG3h98fGxyiMb5vlIYAHDF9T-w_LntlYItohv63MmdYR-hPpAqjXE7KOfErf-wUDGE9R3bfiQ4HpTdyFJB1nsToYrZ9lhP2mzjTCTs58ckZfQ28DFHn_lfHWpR4rJBgvLx7IH4rMrUayr09Ap-PxQLbv0lYMtmgG1z3JK8MXnuYR0UJdZnEIezOzUTlThhCXB-nvuAXYjYxZZTR0FtlgZUHhIpYK0V2abf_Q_Or36akNCUg`
// These variables always result in a valid token
var (
ValidIssuer = "local.com"
ValidSubject = "tim@local.com"
ValidAudience = []string{"unit", "test"}
ValidAuthTime = time.Now().Add(-time.Minute) // authtime is always 1 minute in the past
ValidExpiration = ValidAuthTime.Add(2 * time.Minute) // token is always 1 more minute available
ValidJWTID = "9876"
ValidNonce = "12345"
ValidACR = "something"
ValidAMR = []string{"foo", "bar"}
ValidClientID = "555666"
ValidSkew = time.Second
)
// ValidIDToken returns a token and claims that are in the token.
// It uses the Valid* global variables and the token will always
// pass verification.
func ValidIDToken() (string, *oidc.IDTokenClaims) {
return NewIDToken(ValidIssuer, ValidSubject, ValidAudience, ValidExpiration, ValidAuthTime, ValidNonce, ValidACR, ValidAMR, ValidClientID, ValidSkew, "")
}
// ValidAccessToken returns a token and claims that are in the token.
// It uses the Valid* global variables and the token always passes
// verification within the same test run.
func ValidAccessToken() (string, *oidc.AccessTokenClaims) {
return NewAccessToken(ValidIssuer, ValidSubject, ValidAudience, ValidExpiration, ValidJWTID, ValidClientID, ValidSkew)
}
func ValidJWTProfileAssertion() (string, *oidc.JWTTokenRequest) {
return NewJWTProfileAssertion(ValidClientID, ValidClientID, []string{ValidIssuer}, time.Now(), ValidExpiration)
}
// ACRVerify is a oidc.ACRVerifier func.
func ACRVerify(acr string) error {
if acr != ValidACR {
return errors.New("invalid acr")
}
return nil
}

View file

@ -2,6 +2,7 @@ package client
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -10,44 +11,32 @@ import (
"strings" "strings"
"time" "time"
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/logging"
"go.opentelemetry.io/otel"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto" "github.com/zitadel/oidc/v2/pkg/crypto"
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
var ( var Encoder = httphelper.Encoder(oidc.NewEncoder())
Encoder = httphelper.Encoder(oidc.NewEncoder())
Tracer = otel.Tracer("github.com/zitadel/oidc/pkg/client")
)
// Discover calls the discovery endpoint of the provided issuer and returns its configuration // Discover calls the discovery endpoint of the provided issuer and returns its configuration
// It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url // It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url
func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) { func Discover(issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) {
ctx, span := Tracer.Start(ctx, "Discover")
defer span.End()
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" { if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" {
wellKnown = wellKnownUrl[0] wellKnown = wellKnownUrl[0]
} }
req, err := http.NewRequestWithContext(ctx, http.MethodGet, wellKnown, nil) req, err := http.NewRequest("GET", wellKnown, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
discoveryConfig := new(oidc.DiscoveryConfiguration) discoveryConfig := new(oidc.DiscoveryConfiguration)
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig) err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
if err != nil { if err != nil {
return nil, errors.Join(oidc.ErrDiscoveryFailed, err) return nil, err
} }
if logger, ok := logging.FromContext(ctx); ok {
logger.Debug("discover", "config", discoveryConfig)
}
if discoveryConfig.Issuer != issuer { if discoveryConfig.Issuer != issuer {
return nil, oidc.ErrIssuerInvalid return nil, oidc.ErrIssuerInvalid
} }
@ -59,15 +48,12 @@ type TokenEndpointCaller interface {
HttpClient() *http.Client HttpClient() *http.Client
} }
func CallTokenEndpoint(ctx context.Context, request any, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { func CallTokenEndpoint(request interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) {
return callTokenEndpoint(ctx, request, nil, caller) return callTokenEndpoint(request, nil, caller)
} }
func callTokenEndpoint(ctx context.Context, request any, authFn any, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) { func callTokenEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) {
ctx, span := Tracer.Start(ctx, "callTokenEndpoint") req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn)
defer span.End()
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -75,18 +61,12 @@ func callTokenEndpoint(ctx context.Context, request any, authFn any, caller Toke
if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil { if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil {
return nil, err return nil, err
} }
token := &oauth2.Token{ return &oauth2.Token{
AccessToken: tokenRes.AccessToken, AccessToken: tokenRes.AccessToken,
TokenType: tokenRes.TokenType, TokenType: tokenRes.TokenType,
RefreshToken: tokenRes.RefreshToken, RefreshToken: tokenRes.RefreshToken,
Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second), Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second),
} }, nil
if tokenRes.IDToken != "" {
token = token.WithExtra(map[string]any{
"id_token": tokenRes.IDToken,
})
}
return token, nil
} }
type EndSessionCaller interface { type EndSessionCaller interface {
@ -94,16 +74,8 @@ type EndSessionCaller interface {
HttpClient() *http.Client HttpClient() *http.Client
} }
func CallEndSessionEndpoint(ctx context.Context, request any, authFn any, caller EndSessionCaller) (*url.URL, error) { func CallEndSessionEndpoint(request interface{}, authFn interface{}, caller EndSessionCaller) (*url.URL, error) {
ctx, span := Tracer.Start(ctx, "CallEndSessionEndpoint") req, err := httphelper.FormRequest(caller.GetEndSessionEndpoint(), request, Encoder, authFn)
defer span.End()
endpoint := caller.GetEndSessionEndpoint()
if endpoint == "" {
return nil, fmt.Errorf("end session %w", ErrEndpointNotSet)
}
req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -145,16 +117,8 @@ type RevokeRequest struct {
ClientSecret string `schema:"client_secret"` ClientSecret string `schema:"client_secret"`
} }
func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller RevokeCaller) error { func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCaller) error {
ctx, span := Tracer.Start(ctx, "CallRevokeEndpoint") req, err := httphelper.FormRequest(caller.GetRevokeEndpoint(), request, Encoder, authFn)
defer span.End()
endpoint := caller.GetRevokeEndpoint()
if endpoint == "" {
return fmt.Errorf("revoke %w", ErrEndpointNotSet)
}
req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn)
if err != nil { if err != nil {
return err return err
} }
@ -181,11 +145,8 @@ func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller Rev
return nil return nil
} }
func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) { func CallTokenExchangeEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) {
ctx, span := Tracer.Start(ctx, "CallTokenExchangeEndpoint") req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn)
defer span.End()
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -197,12 +158,12 @@ func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, cal
} }
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
privateKey, algorithm, err := crypto.BytesToPrivateKey(key) privateKey, err := crypto.BytesToPrivateKey(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
signingKey := jose.SigningKey{ signingKey := jose.SigningKey{
Algorithm: algorithm, Algorithm: jose.RS256,
Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID}, Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID},
} }
return jose.NewSigner(signingKey, &jose.SignerOptions{}) return jose.NewSigner(signingKey, &jose.SignerOptions{})
@ -215,8 +176,8 @@ func SignedJWTProfileAssertion(clientID string, audience []string, expiration ti
Issuer: clientID, Issuer: clientID,
Subject: clientID, Subject: clientID,
Audience: audience, Audience: audience,
ExpiresAt: oidc.FromTime(exp), ExpiresAt: oidc.Time(exp),
IssuedAt: oidc.FromTime(iat), IssuedAt: oidc.Time(iat),
}, signer) }, signer)
} }
@ -225,16 +186,8 @@ type DeviceAuthorizationCaller interface {
HttpClient() *http.Client HttpClient() *http.Client
} }
func CallDeviceAuthorizationEndpoint(ctx context.Context, request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller, authFn any) (*oidc.DeviceAuthorizationResponse, error) { func CallDeviceAuthorizationEndpoint(request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) {
ctx, span := Tracer.Start(ctx, "CallDeviceAuthorizationEndpoint") req, err := httphelper.FormRequest(caller.GetDeviceAuthorizationEndpoint(), request, Encoder, nil)
defer span.End()
endpoint := caller.GetDeviceAuthorizationEndpoint()
if endpoint == "" {
return nil, fmt.Errorf("device authorization %w", ErrEndpointNotSet)
}
req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -255,10 +208,7 @@ type DeviceAccessTokenRequest struct {
} }
func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
ctx, span := Tracer.Start(ctx, "CallDeviceAccessTokenEndpoint") req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, nil)
defer span.End()
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -266,17 +216,28 @@ func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTok
req.SetBasicAuth(request.ClientID, request.ClientSecret) req.SetBasicAuth(request.ClientID, request.ClientSecret)
} }
resp := new(oidc.AccessTokenResponse) httpResp, err := caller.HttpClient().Do(req)
if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil { if err != nil {
return nil, err return nil, err
} }
return resp, nil defer httpResp.Body.Close()
resp := new(struct {
*oidc.AccessTokenResponse
*oidc.Error
})
if err = json.NewDecoder(httpResp.Body).Decode(resp); err != nil {
return nil, err
}
if httpResp.StatusCode == http.StatusOK {
return resp.AccessTokenResponse, nil
}
return nil, resp.Error
} }
func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
ctx, span := Tracer.Start(ctx, "PollDeviceAccessTokenEndpoint")
defer span.End()
for { for {
timer := time.After(interval) timer := time.After(interval)
select { select {

View file

@ -1,59 +0,0 @@
package client
import (
"context"
"net/http"
"testing"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDiscover(t *testing.T) {
type wantFields struct {
UILocalesSupported bool
}
type args struct {
issuer string
wellKnownUrl []string
}
tests := []struct {
name string
args args
wantFields *wantFields
wantErr error
}{
{
name: "spotify", // https://github.com/zitadel/oidc/issues/406
args: args{
issuer: "https://accounts.spotify.com",
},
wantFields: &wantFields{
UILocalesSupported: true,
},
wantErr: nil,
},
{
name: "discovery failed",
args: args{
issuer: "https://example.com",
},
wantErr: oidc.ErrDiscoveryFailed,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Discover(context.Background(), tt.args.issuer, http.DefaultClient, tt.args.wellKnownUrl...)
require.ErrorIs(t, err, tt.wantErr)
if tt.wantFields == nil {
return
}
assert.Equal(t, tt.args.issuer, got.Issuer)
if tt.wantFields.UILocalesSupported {
assert.NotEmpty(t, got.UILocalesSupported)
}
})
}
}

View file

@ -1,5 +0,0 @@
package client
import "errors"
var ErrEndpointNotSet = errors.New("endpoint not set")

View file

@ -2,65 +2,32 @@ package client_test
import ( import (
"bytes" "bytes"
"context"
"fmt"
"io" "io"
"log/slog" "io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os" "os"
"os/signal"
"strconv" "strconv"
"syscall"
"testing" "testing"
"time" "time"
"github.com/jeremija/gosubmit" "github.com/jeremija/gosubmit"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/exampleop" "github.com/zitadel/oidc/v2/example/server/exampleop"
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage" "github.com/zitadel/oidc/v2/example/server/storage"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/client/rp"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs" "github.com/zitadel/oidc/v2/pkg/client/rs"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/tokenexchange" "github.com/zitadel/oidc/v2/pkg/client/tokenexchange"
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
) )
var Logger = slog.New(
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}),
)
var CTX context.Context
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT)
defer cancel()
CTX, cancel = context.WithTimeout(ctx, time.Minute)
defer cancel()
return m.Run()
}())
}
func TestRelyingPartySession(t *testing.T) { func TestRelyingPartySession(t *testing.T) {
for _, wrapServer := range []bool{false, true} {
t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) {
testRelyingPartySession(t, wrapServer)
})
}
}
func testRelyingPartySession(t *testing.T, wrapServer bool) {
t.Log("------- start example OP ------") t.Log("------- start example OP ------")
targetURL := "http://local-site" targetURL := "http://local-site"
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
@ -68,17 +35,17 @@ func testRelyingPartySession(t *testing.T, wrapServer bool) {
opServer := httptest.NewServer(&dh) opServer := httptest.NewServer(&dh)
defer opServer.Close() defer opServer.Close()
t.Logf("auth server at %s", opServer.URL) t.Logf("auth server at %s", opServer.URL)
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, wrapServer) dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage)
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
t.Log("------- run authorization code flow ------") t.Log("------- run authorization code flow ------")
provider, tokens := RunAuthorizationCodeFlow(t, opServer, clientID, "secret") provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, "secret")
t.Log("------- refresh tokens ------") t.Log("------- refresh tokens ------")
newTokens, err := rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "") newTokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "")
require.NoError(t, err, "refresh token") require.NoError(t, err, "refresh token")
assert.NotNil(t, newTokens, "access token") assert.NotNil(t, newTokens, "access token")
t.Logf("new access token %s", newTokens.AccessToken) t.Logf("new access token %s", newTokens.AccessToken)
@ -86,13 +53,10 @@ func testRelyingPartySession(t *testing.T, wrapServer bool) {
t.Logf("new token type %s", newTokens.TokenType) t.Logf("new token type %s", newTokens.TokenType)
t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339)) t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339))
require.NotEmpty(t, newTokens.AccessToken, "new accessToken") require.NotEmpty(t, newTokens.AccessToken, "new accessToken")
assert.NotEmpty(t, newTokens.IDToken, "new idToken")
assert.NotNil(t, newTokens.IDTokenClaims)
assert.Equal(t, newTokens.IDTokenClaims.Subject, tokens.IDTokenClaims.Subject)
t.Log("------ end session (logout) ------") t.Log("------ end session (logout) ------")
newLoc, err := rp.EndSession(CTX, provider, tokens.IDToken, "", "") newLoc, err := rp.EndSession(provider, idToken, "", "")
require.NoError(t, err, "logout") require.NoError(t, err, "logout")
if newLoc != nil { if newLoc != nil {
t.Logf("redirect to %s", newLoc) t.Logf("redirect to %s", newLoc)
@ -101,111 +65,17 @@ func testRelyingPartySession(t *testing.T, wrapServer bool) {
} }
t.Log("------ attempt refresh again (should fail) ------") t.Log("------ attempt refresh again (should fail) ------")
t.Log("trying original refresh token", tokens.RefreshToken) t.Log("trying original refresh token", refreshToken)
_, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "") _, err = rp.RefreshAccessToken(provider, refreshToken, "", "")
assert.Errorf(t, err, "refresh with original") assert.Errorf(t, err, "refresh with original")
if newTokens.RefreshToken != "" { if newTokens.RefreshToken != "" {
t.Log("trying replacement refresh token", newTokens.RefreshToken) t.Log("trying replacement refresh token", newTokens.RefreshToken)
_, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, newTokens.RefreshToken, "", "") _, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "")
assert.Errorf(t, err, "refresh with replacement") assert.Errorf(t, err, "refresh with replacement")
} }
} }
func TestRelyingPartyWithSigningAlgsFromDiscovery(t *testing.T) {
targetURL := "http://local-site"
localURL, err := url.Parse(targetURL + "/login?requestID=1234")
require.NoError(t, err, "local url")
t.Log("------- start example OP ------")
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
clientSecret := "secret"
client := storage.WebClient(clientID, clientSecret, targetURL)
storage.RegisterClients(client)
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
var dh deferredHandler
opServer := httptest.NewServer(&dh)
defer opServer.Close()
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, true)
t.Log("------- create RP ------")
provider, err := rp.NewRelyingPartyOIDC(
CTX,
opServer.URL,
clientID,
clientSecret,
targetURL,
[]string{"openid"},
rp.WithSigningAlgsFromDiscovery(),
)
require.NoError(t, err, "new rp")
t.Log("------- run authorization code flow ------")
jar, err := cookiejar.New(nil)
require.NoError(t, err, "create cookie jar")
httpClient := &http.Client{
Timeout: time.Second * 5,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
Jar: jar,
}
state := "state-" + strconv.FormatInt(seed.Int63(), 25)
capturedW := httptest.NewRecorder()
get := httptest.NewRequest("GET", localURL.String(), nil)
rp.AuthURLHandler(func() string { return state }, provider,
rp.WithPromptURLParam("Hello, World!", "Goodbye, World!"),
rp.WithURLParam("custom", "param"),
)(capturedW, get)
defer func() {
if t.Failed() {
t.Log("response body (redirect from RP to OP)", capturedW.Body.String())
}
}()
resp := capturedW.Result()
startAuthURL, err := resp.Location()
require.NoError(t, err, "get redirect")
loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL)
form := getForm(t, "get login form", httpClient, loginPageURL)
defer func() {
if t.Failed() {
t.Logf("login form (unfilled): %s", string(form))
}
}()
postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL,
gosubmit.Set("username", "test-user@local-site"),
gosubmit.Set("password", "verysecure"),
)
codeBearingURL := getRedirect(t, "get redirect with code", httpClient, postLoginRedirectURL)
capturedW = httptest.NewRecorder()
get = httptest.NewRequest("GET", codeBearingURL.String(), nil)
var idToken string
redirect := func(w http.ResponseWriter, r *http.Request, newTokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
idToken = newTokens.IDToken
http.Redirect(w, r, targetURL, http.StatusFound)
}
rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get)
defer func() {
if t.Failed() {
t.Log("token exchange response body", capturedW.Body.String())
require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code")
}
}()
t.Log("------- verify id token ------")
_, err = rp.VerifyIDToken[*oidc.IDTokenClaims](CTX, idToken, provider.IDTokenVerifier())
require.NoError(t, err, "verify id token")
}
func TestResourceServerTokenExchange(t *testing.T) { func TestResourceServerTokenExchange(t *testing.T) {
for _, wrapServer := range []bool{false, true} {
t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) {
testResourceServerTokenExchange(t, wrapServer)
})
}
}
func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
t.Log("------- start example OP ------") t.Log("------- start example OP ------")
targetURL := "http://local-site" targetURL := "http://local-site"
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
@ -213,24 +83,23 @@ func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
opServer := httptest.NewServer(&dh) opServer := httptest.NewServer(&dh)
defer opServer.Close() defer opServer.Close()
t.Logf("auth server at %s", opServer.URL) t.Logf("auth server at %s", opServer.URL)
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, wrapServer) dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage)
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
clientSecret := "secret" clientSecret := "secret"
t.Log("------- run authorization code flow ------") t.Log("------- run authorization code flow ------")
provider, tokens := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret) provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret)
resourceServer, err := rs.NewResourceServerClientCredentials(CTX, opServer.URL, clientID, clientSecret) resourceServer, err := rs.NewResourceServerClientCredentials(opServer.URL, clientID, clientSecret)
require.NoError(t, err, "new resource server") require.NoError(t, err, "new resource server")
t.Log("------- exchage refresh tokens (impersonation) ------") t.Log("------- exchage refresh tokens (impersonation) ------")
tokenExchangeResponse, err := tokenexchange.ExchangeToken( tokenExchangeResponse, err := tokenexchange.ExchangeToken(
CTX,
resourceServer, resourceServer,
tokens.RefreshToken, refreshToken,
oidc.RefreshTokenType, oidc.RefreshTokenType,
"", "",
"", "",
@ -248,7 +117,7 @@ func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
t.Log("------ end session (logout) ------") t.Log("------ end session (logout) ------")
newLoc, err := rp.EndSession(CTX, provider, tokens.IDToken, "", "") newLoc, err := rp.EndSession(provider, idToken, "", "")
require.NoError(t, err, "logout") require.NoError(t, err, "logout")
if newLoc != nil { if newLoc != nil {
t.Logf("redirect to %s", newLoc) t.Logf("redirect to %s", newLoc)
@ -259,9 +128,8 @@ func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
t.Log("------- attempt exchage again (should fail) ------") t.Log("------- attempt exchage again (should fail) ------")
tokenExchangeResponse, err = tokenexchange.ExchangeToken( tokenExchangeResponse, err = tokenexchange.ExchangeToken(
CTX,
resourceServer, resourceServer,
tokens.RefreshToken, refreshToken,
oidc.RefreshTokenType, oidc.RefreshTokenType,
"", "",
"", "",
@ -273,9 +141,10 @@ func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
require.Error(t, err, "refresh token") require.Error(t, err, "refresh token")
assert.Contains(t, err.Error(), "subject_token is invalid") assert.Contains(t, err.Error(), "subject_token is invalid")
require.Nil(t, tokenExchangeResponse, "token exchange response") require.Nil(t, tokenExchangeResponse, "token exchange response")
} }
func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, clientSecret string) (provider rp.RelyingParty, tokens *oidc.Tokens[*oidc.IDTokenClaims]) { func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, clientSecret string) (provider rp.RelyingParty, accessToken, refreshToken, idToken string) {
targetURL := "http://local-site" targetURL := "http://local-site"
localURL, err := url.Parse(targetURL + "/login?requestID=1234") localURL, err := url.Parse(targetURL + "/login?requestID=1234")
require.NoError(t, err, "local url") require.NoError(t, err, "local url")
@ -297,14 +166,12 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
key := []byte("test1234test1234") key := []byte("test1234test1234")
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
provider, err = rp.NewRelyingPartyOIDC( provider, err = rp.NewRelyingPartyOIDC(
CTX,
opServer.URL, opServer.URL,
clientID, clientID,
clientSecret, clientSecret,
targetURL, targetURL,
[]string{"openid", "email", "profile", "offline_access"}, []string{"openid", "email", "profile", "offline_access"},
rp.WithPKCE(cookieHandler), rp.WithPKCE(cookieHandler),
rp.WithAuthStyle(oauth2.AuthStyleInHeader),
rp.WithVerifierOpts( rp.WithVerifierOpts(
rp.WithIssuedAtOffset(5*time.Second), rp.WithIssuedAtOffset(5*time.Second),
rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"), rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"),
@ -317,10 +184,7 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
state := "state-" + strconv.FormatInt(seed.Int63(), 25) state := "state-" + strconv.FormatInt(seed.Int63(), 25)
capturedW := httptest.NewRecorder() capturedW := httptest.NewRecorder()
get := httptest.NewRequest("GET", localURL.String(), nil) get := httptest.NewRequest("GET", localURL.String(), nil)
rp.AuthURLHandler(func() string { return state }, provider, rp.AuthURLHandler(func() string { return state }, provider)(capturedW, get)
rp.WithPromptURLParam("Hello, World!", "Goodbye, World!"),
rp.WithURLParam("custom", "param"),
)(capturedW, get)
defer func() { defer func() {
if t.Failed() { if t.Failed() {
@ -329,8 +193,6 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
}() }()
require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code") require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code")
require.Less(t, capturedW.Code, 400, "captured response code") require.Less(t, capturedW.Code, 400, "captured response code")
require.Contains(t, capturedW.Body.String(), `prompt=Hello%2C+World%21+Goodbye%2C+World%21`)
require.Contains(t, capturedW.Body.String(), `custom=param`)
//nolint:bodyclose //nolint:bodyclose
resp := capturedW.Result() resp := capturedW.Result()
@ -373,19 +235,21 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
} }
var email string var email string
redirect := func(w http.ResponseWriter, r *http.Request, newTokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
tokens = newTokens
require.NotNil(t, tokens, "tokens") require.NotNil(t, tokens, "tokens")
require.NotNil(t, info, "info") require.NotNil(t, info, "info")
t.Log("access token", tokens.AccessToken) t.Log("access token", tokens.AccessToken)
t.Log("refresh token", tokens.RefreshToken) t.Log("refresh token", tokens.RefreshToken)
t.Log("id token", tokens.IDToken) t.Log("id token", tokens.IDToken)
t.Log("email", info.Email) t.Log("email", info.GetEmail())
email = info.Email accessToken = tokens.AccessToken
http.Redirect(w, r, targetURL, 302) refreshToken = tokens.RefreshToken
idToken = tokens.IDToken
email = info.GetEmail()
http.Redirect(w, r, targetURL, http.StatusFound)
} }
rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider, rp.WithURLParam("custom", "param"))(capturedW, get) rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get)
defer func() { defer func() {
if t.Failed() { if t.Failed() {
@ -394,7 +258,7 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
} }
}() }()
require.Less(t, capturedW.Code, 400, "token exchange response code") require.Less(t, capturedW.Code, 400, "token exchange response code")
// TODO: how to check the custom header was sent to the server? require.Less(t, capturedW.Code, 400, "token exchange response code")
//nolint:bodyclose //nolint:bodyclose
resp = capturedW.Result() resp = capturedW.Result()
@ -403,124 +267,12 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
require.NoError(t, err, "get fully-authorizied redirect location") require.NoError(t, err, "get fully-authorizied redirect location")
require.Equal(t, targetURL, authorizedURL.String(), "fully-authorizied redirect location") require.Equal(t, targetURL, authorizedURL.String(), "fully-authorizied redirect location")
require.NotEmpty(t, tokens.IDToken, "id token") require.NotEmpty(t, idToken, "id token")
assert.NotEmpty(t, tokens.RefreshToken, "refresh token") assert.NotEmpty(t, refreshToken, "refresh token")
assert.NotEmpty(t, tokens.AccessToken, "access token") assert.NotEmpty(t, accessToken, "access token")
assert.NotEmpty(t, email, "email") assert.NotEmpty(t, email, "email")
return provider, tokens return provider, accessToken, refreshToken, idToken
}
func TestClientCredentials(t *testing.T) {
targetURL := "http://local-site"
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
var dh deferredHandler
opServer := httptest.NewServer(&dh)
defer opServer.Close()
t.Logf("auth server at %s", opServer.URL)
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, true)
provider, err := rp.NewRelyingPartyOIDC(
CTX,
opServer.URL,
"sid1",
"verysecret",
targetURL,
[]string{"openid"},
)
require.NoError(t, err, "new rp")
token, err := rp.ClientCredentials(CTX, provider, nil)
require.NoError(t, err, "ClientCredentials call")
require.NotNil(t, token)
assert.NotEmpty(t, token.AccessToken)
}
func TestErrorFromPromptNone(t *testing.T) {
jar, err := cookiejar.New(nil)
require.NoError(t, err, "create cookie jar")
httpClient := &http.Client{
Timeout: time.Second * 5,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
Jar: jar,
}
t.Log("------- start example OP ------")
targetURL := "http://local-site"
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
var dh deferredHandler
opServer := httptest.NewServer(&dh)
defer opServer.Close()
t.Logf("auth server at %s", opServer.URL)
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, false, op.WithHttpInterceptors(
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Logf("request to %s", r.URL)
next.ServeHTTP(w, r)
})
},
))
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
clientSecret := "secret"
client := storage.WebClient(clientID, clientSecret, targetURL)
storage.RegisterClients(client)
t.Log("------- create RP ------")
key := []byte("test1234test1234")
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
provider, err := rp.NewRelyingPartyOIDC(
CTX,
opServer.URL,
clientID,
clientSecret,
targetURL,
[]string{"openid", "email", "profile", "offline_access"},
rp.WithPKCE(cookieHandler),
rp.WithVerifierOpts(
rp.WithIssuedAtOffset(5*time.Second),
rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"),
),
)
require.NoError(t, err, "new rp")
t.Log("------- start auth flow with prompt=none ------- ")
state := "state-32892"
capturedW := httptest.NewRecorder()
localURL, err := url.Parse(targetURL + "/login")
require.NoError(t, err)
get := httptest.NewRequest("GET", localURL.String(), nil)
rp.AuthURLHandler(func() string { return state }, provider,
rp.WithPromptURLParam("none"),
rp.WithResponseModeURLParam(oidc.ResponseModeFragment),
)(capturedW, get)
defer func() {
if t.Failed() {
t.Log("response body (redirect from RP to OP)", capturedW.Body.String())
}
}()
require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code")
require.Less(t, capturedW.Code, 400, "captured response code")
//nolint:bodyclose
resp := capturedW.Result()
jar.SetCookies(localURL, resp.Cookies())
startAuthURL, err := resp.Location()
require.NoError(t, err, "get redirect")
assert.NotEmpty(t, startAuthURL, "login url")
t.Log("Starting auth at", startAuthURL)
t.Log("------- get redirect from OP ------")
loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL)
t.Log("login page URL", loginPageURL)
require.Contains(t, loginPageURL.String(), `error=login_required`, "prompt=none should error")
require.Contains(t, loginPageURL.String(), `local-site#error=`, "response_mode=fragment means '#' instead of '?'")
} }
type deferredHandler struct { type deferredHandler struct {
@ -568,7 +320,7 @@ func getForm(t *testing.T, desc string, httpClient *http.Client, uri *url.URL) [
func fillForm(t *testing.T, desc string, httpClient *http.Client, body []byte, uri *url.URL, opts ...gosubmit.Option) *url.URL { func fillForm(t *testing.T, desc string, httpClient *http.Client, body []byte, uri *url.URL, opts ...gosubmit.Option) *url.URL {
// TODO: switch to io.NopCloser when go1.15 support is dropped // TODO: switch to io.NopCloser when go1.15 support is dropped
req := gosubmit.ParseWithURL(io.NopCloser(bytes.NewReader(body)), uri.String()).FirstForm().Testing(t).NewTestRequest( req := gosubmit.ParseWithURL(ioutil.NopCloser(bytes.NewReader(body)), uri.String()).FirstForm().Testing(t).NewTestRequest(
append([]gosubmit.Option{gosubmit.AutoFill()}, opts...)..., append([]gosubmit.Option{gosubmit.AutoFill()}, opts...)...,
) )
if req.URL.Scheme == "" { if req.URL.Scheme == "" {

View file

@ -1,18 +1,17 @@
package client package client
import ( import (
"context"
"net/url" "net/url"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" "github.com/zitadel/oidc/v2/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
// JWTProfileExchange handles the oauth2 jwt profile exchange // JWTProfileExchange handles the oauth2 jwt profile exchange
func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller TokenEndpointCaller) (*oauth2.Token, error) { func JWTProfileExchange(jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller TokenEndpointCaller) (*oauth2.Token, error) {
return CallTokenEndpoint(ctx, jwtProfileGrantRequest, caller) return CallTokenEndpoint(jwtProfileGrantRequest, caller)
} }
func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption { func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption {

View file

@ -2,7 +2,7 @@ package client
import ( import (
"encoding/json" "encoding/json"
"os" "io/ioutil"
) )
const ( const (
@ -10,7 +10,7 @@ const (
applicationKey = "application" applicationKey = "application"
) )
type KeyFile struct { type keyFile struct {
Type string `json:"type"` // serviceaccount or application Type string `json:"type"` // serviceaccount or application
KeyID string `json:"keyId"` KeyID string `json:"keyId"`
Key string `json:"key"` Key string `json:"key"`
@ -23,16 +23,16 @@ type KeyFile struct {
ClientID string `json:"clientId"` ClientID string `json:"clientId"`
} }
func ConfigFromKeyFile(path string) (*KeyFile, error) { func ConfigFromKeyFile(path string) (*keyFile, error) {
data, err := os.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return ConfigFromKeyFileData(data) return ConfigFromKeyFileData(data)
} }
func ConfigFromKeyFileData(data []byte) (*KeyFile, error) { func ConfigFromKeyFileData(data []byte) (*keyFile, error) {
var f KeyFile var f keyFile
if err := json.Unmarshal(data, &f); err != nil { if err := json.Unmarshal(data, &f); err != nil {
return nil, err return nil, err
} }

View file

@ -1,25 +1,19 @@
package profile package profile
import ( import (
"context"
"net/http" "net/http"
"time" "time"
jose "github.com/go-jose/go-jose/v4"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" "github.com/zitadel/oidc/v2/pkg/client"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type TokenSource interface {
oauth2.TokenSource
TokenCtx(context.Context) (*oauth2.Token, error)
}
// jwtProfileTokenSource implement the oauth2.TokenSource // jwtProfileTokenSource implement the oauth2.TokenSource
// it will request a token using the OAuth2 JWT Profile Grant // it will request a token using the OAuth2 JWT Profile Grant
// therefore sending an `assertion` by signing a JWT with the provided private key // therefore sending an `assertion` by singing a JWT with the provided private key
type jwtProfileTokenSource struct { type jwtProfileTokenSource struct {
clientID string clientID string
audience []string audience []string
@ -29,38 +23,23 @@ type jwtProfileTokenSource struct {
tokenEndpoint string tokenEndpoint string
} }
// NewJWTProfileTokenSourceFromKeyFile returns an implementation of TokenSource func NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath string, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) {
// It will request a token using the OAuth2 JWT Profile Grant, keyData, err := client.ConfigFromKeyFile(keyPath)
// therefore sending an `assertion` by singing a JWT with the provided private key from jsonFile.
//
// The passed context is only used for the call to the Discover endpoint.
func NewJWTProfileTokenSourceFromKeyFile(ctx context.Context, issuer, jsonFile string, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) {
keyData, err := client.ConfigFromKeyFile(jsonFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewJWTProfileTokenSource(ctx, issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
} }
// NewJWTProfileTokenSourceFromKeyFileData returns an implementation of oauth2.TokenSource func NewJWTProfileTokenSourceFromKeyFileData(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) {
// It will request a token using the OAuth2 JWT Profile Grant, keyData, err := client.ConfigFromKeyFileData(data)
// therefore sending an `assertion` by singing a JWT with the provided private key in jsonData.
//
// The passed context is only used for the call to the Discover endpoint.
func NewJWTProfileTokenSourceFromKeyFileData(ctx context.Context, issuer string, jsonData []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) {
keyData, err := client.ConfigFromKeyFileData(jsonData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewJWTProfileTokenSource(ctx, issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
} }
// NewJWTProfileSource returns an implementation of oauth2.TokenSource func NewJWTProfileTokenSource(issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) {
// It will request a token using the OAuth2 JWT Profile Grant,
// therefore sending an `assertion` by singing a JWT with the provided private key.
//
// The passed context is only used for the call to the Discover endpoint.
func NewJWTProfileTokenSource(ctx context.Context, issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) {
signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) signer, err := client.NewSignerFromPrivateKeyByte(key, keyID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -76,7 +55,7 @@ func NewJWTProfileTokenSource(ctx context.Context, issuer, clientID, keyID strin
opt(source) opt(source)
} }
if source.tokenEndpoint == "" { if source.tokenEndpoint == "" {
config, err := client.Discover(ctx, issuer, source.httpClient) config, err := client.Discover(issuer, source.httpClient)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -85,13 +64,13 @@ func NewJWTProfileTokenSource(ctx context.Context, issuer, clientID, keyID strin
return source, nil return source, nil
} }
func WithHTTPClient(client *http.Client) func(source *jwtProfileTokenSource) { func WithHTTPClient(client *http.Client) func(*jwtProfileTokenSource) {
return func(source *jwtProfileTokenSource) { return func(source *jwtProfileTokenSource) {
source.httpClient = client source.httpClient = client
} }
} }
func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(source *jwtProfileTokenSource) { func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*jwtProfileTokenSource) {
return func(source *jwtProfileTokenSource) { return func(source *jwtProfileTokenSource) {
source.tokenEndpoint = tokenEndpoint source.tokenEndpoint = tokenEndpoint
} }
@ -106,13 +85,9 @@ func (j *jwtProfileTokenSource) HttpClient() *http.Client {
} }
func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) { func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) {
return j.TokenCtx(context.Background())
}
func (j *jwtProfileTokenSource) TokenCtx(ctx context.Context) (*oauth2.Token, error) {
assertion, err := client.SignedJWTProfileAssertion(j.clientID, j.audience, time.Hour, j.signer) assertion, err := client.SignedJWTProfileAssertion(j.clientID, j.audience, time.Hour, j.signer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return client.JWTProfileExchange(ctx, oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j) return client.JWTProfileExchange(oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j)
} }

View file

@ -4,22 +4,22 @@ import (
"context" "context"
"net/http" "net/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/client/rp"
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
const ( const (
loginPath = "/login" loginPath = "/login"
) )
func CodeFlow[C oidc.IDClaims](ctx context.Context, relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens[C] { func CodeFlow(ctx context.Context, relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens {
codeflowCtx, codeflowCancel := context.WithCancel(ctx) codeflowCtx, codeflowCancel := context.WithCancel(ctx)
defer codeflowCancel() defer codeflowCancel()
tokenChan := make(chan *oidc.Tokens[C], 1) tokenChan := make(chan *oidc.Tokens, 1)
callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp rp.RelyingParty) { callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) {
tokenChan <- tokens tokenChan <- tokens
msg := "<p><strong>Success!</strong></p>" msg := "<p><strong>Success!</strong></p>"
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>" msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"

View file

@ -1,7 +1,7 @@
package rp package rp
import ( import (
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc/grants/tokenexchange" "github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
) )
// DelegationTokenRequest is an implementation of TokenExchangeRequest // DelegationTokenRequest is an implementation of TokenExchangeRequest

View file

@ -5,13 +5,14 @@ import (
"fmt" "fmt"
"time" "time"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" "github.com/zitadel/oidc/v2/pkg/client"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) { func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) {
confg := rp.OAuthConfig() confg := rp.OAuthConfig()
req := &oidc.ClientCredentialsRequest{ req := &oidc.ClientCredentialsRequest{
GrantType: oidc.GrantTypeDeviceCode,
Scope: scopes, Scope: scopes,
ClientID: confg.ClientID, ClientID: confg.ClientID,
ClientSecret: confg.ClientSecret, ClientSecret: confg.ClientSecret,
@ -32,27 +33,19 @@ func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.
// DeviceAuthorization starts a new Device Authorization flow as defined // DeviceAuthorization starts a new Device Authorization flow as defined
// in RFC 8628, section 3.1 and 3.2: // in RFC 8628, section 3.1 and 3.2:
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1 // https://www.rfc-editor.org/rfc/rfc8628#section-3.1
func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, authFn any) (*oidc.DeviceAuthorizationResponse, error) { func DeviceAuthorization(scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) {
ctx, span := client.Tracer.Start(ctx, "DeviceAuthorization")
defer span.End()
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAuthorization")
req, err := newDeviceClientCredentialsRequest(scopes, rp) req, err := newDeviceClientCredentialsRequest(scopes, rp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return client.CallDeviceAuthorizationEndpoint(ctx, req, rp, authFn) return client.CallDeviceAuthorizationEndpoint(req, rp)
} }
// DeviceAccessToken attempts to obtain tokens from a Device Authorization, // DeviceAccessToken attempts to obtain tokens from a Device Authorization,
// by means of polling as defined in RFC, section 3.3 and 3.4: // by means of polling as defined in RFC, section 3.3 and 3.4:
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4 // https://www.rfc-editor.org/rfc/rfc8628#section-3.4
func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) { func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) {
ctx, span := client.Tracer.Start(ctx, "DeviceAccessToken")
defer span.End()
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAccessToken")
req := &client.DeviceAccessTokenRequest{ req := &client.DeviceAccessTokenRequest{
DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{ DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{
GrantType: oidc.GrantTypeDeviceCode, GrantType: oidc.GrantTypeDeviceCode,

View file

@ -1,5 +0,0 @@
package rp
import "errors"
var ErrRelyingPartyNotSupportRevokeCaller = errors.New("RelyingParty does not support RevokeCaller")

View file

@ -7,11 +7,10 @@ import (
"net/http" "net/http"
"sync" "sync"
jose "github.com/go-jose/go-jose/v4" "gopkg.in/square/go-jose.v2"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" httphelper "github.com/zitadel/oidc/v2/pkg/http"
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" "github.com/zitadel/oidc/v2/pkg/oidc"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet { func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet {
@ -84,9 +83,6 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) {
} }
func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
ctx, span := client.Tracer.Start(ctx, "VerifySignature")
defer span.End()
keyID, alg := oidc.GetKeyIDAndAlg(jws) keyID, alg := oidc.GetKeyIDAndAlg(jws)
if alg == "" { if alg == "" {
alg = r.defaultAlg alg = r.defaultAlg
@ -139,9 +135,6 @@ func (r *remoteKeySet) exactMatch(jwkID, jwsID string) bool {
} }
func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) { func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) {
ctx, span := client.Tracer.Start(ctx, "verifySignatureRemote")
defer span.End()
keys, err := r.keysFromRemote(ctx) keys, err := r.keysFromRemote(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to fetch key for signature validation: %w", err) return nil, fmt.Errorf("unable to fetch key for signature validation: %w", err)
@ -166,9 +159,6 @@ func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey) {
// keysFromRemote syncs the key set from the remote set, records the values in the // keysFromRemote syncs the key set from the remote set, records the values in the
// cache, and returns the key set. // cache, and returns the key set.
func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) { func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) {
ctx, span := client.Tracer.Start(ctx, "keysFromRemote")
defer span.End()
// Need to lock to inspect the inflight request field. // Need to lock to inspect the inflight request field.
r.mu.Lock() r.mu.Lock()
// If there's not a current inflight request, create one. // If there's not a current inflight request, create one.
@ -192,9 +182,6 @@ func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e
} }
func (r *remoteKeySet) updateKeys(ctx context.Context) { func (r *remoteKeySet) updateKeys(ctx context.Context) {
ctx, span := client.Tracer.Start(ctx, "updateKeys")
defer span.End()
// Sync keys and finish inflight when that's done. // Sync keys and finish inflight when that's done.
keys, err := r.fetchRemoteKeys(ctx) keys, err := r.fetchRemoteKeys(ctx)
@ -214,10 +201,7 @@ func (r *remoteKeySet) updateKeys(ctx context.Context) {
} }
func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, error) { func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, error) {
ctx, span := client.Tracer.Start(ctx, "fetchRemoteKeys") req, err := http.NewRequest("GET", r.jwksURL, nil)
defer span.End()
req, err := http.NewRequestWithContext(ctx, "GET", r.jwksURL, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("oidc: can't create request: %v", err) return nil, fmt.Errorf("oidc: can't create request: %v", err)
} }

View file

@ -1,17 +0,0 @@
package rp
import (
"context"
"log/slog"
"github.com/zitadel/logging"
)
func logCtxWithRPData(ctx context.Context, rp RelyingParty, attrs ...any) context.Context {
logger, ok := rp.Logger(ctx)
if !ok {
return ctx
}
logger = logger.With(slog.Group("rp", attrs...))
return logging.ToContext(ctx, logger)
}

View file

@ -0,0 +1,3 @@
package mock
//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/zitadel/oidc/v2/pkg/client/rp IDTokenVerifier

View file

@ -0,0 +1,163 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/oidc/v2/pkg/client/rp (interfaces: IDTokenVerifier)
// Package mock is a generated GoMock package.
package mock
import (
context "context"
reflect "reflect"
time "time"
gomock "github.com/golang/mock/gomock"
oidc "github.com/zitadel/oidc/v2/pkg/oidc"
)
// MockIDTokenVerifier is a mock of IDTokenVerifier interface.
type MockIDTokenVerifier struct {
ctrl *gomock.Controller
recorder *MockIDTokenVerifierMockRecorder
}
// MockIDTokenVerifierMockRecorder is the mock recorder for MockIDTokenVerifier.
type MockIDTokenVerifierMockRecorder struct {
mock *MockIDTokenVerifier
}
// NewMockIDTokenVerifier creates a new mock instance.
func NewMockIDTokenVerifier(ctrl *gomock.Controller) *MockIDTokenVerifier {
mock := &MockIDTokenVerifier{ctrl: ctrl}
mock.recorder = &MockIDTokenVerifierMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIDTokenVerifier) EXPECT() *MockIDTokenVerifierMockRecorder {
return m.recorder
}
// ACR mocks base method.
func (m *MockIDTokenVerifier) ACR() oidc.ACRVerifier {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ACR")
ret0, _ := ret[0].(oidc.ACRVerifier)
return ret0
}
// ACR indicates an expected call of ACR.
func (mr *MockIDTokenVerifierMockRecorder) ACR() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACR", reflect.TypeOf((*MockIDTokenVerifier)(nil).ACR))
}
// ClientID mocks base method.
func (m *MockIDTokenVerifier) ClientID() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientID")
ret0, _ := ret[0].(string)
return ret0
}
// ClientID indicates an expected call of ClientID.
func (mr *MockIDTokenVerifierMockRecorder) ClientID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientID", reflect.TypeOf((*MockIDTokenVerifier)(nil).ClientID))
}
// Issuer mocks base method.
func (m *MockIDTokenVerifier) Issuer() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Issuer")
ret0, _ := ret[0].(string)
return ret0
}
// Issuer indicates an expected call of Issuer.
func (mr *MockIDTokenVerifierMockRecorder) Issuer() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockIDTokenVerifier)(nil).Issuer))
}
// KeySet mocks base method.
func (m *MockIDTokenVerifier) KeySet() oidc.KeySet {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeySet")
ret0, _ := ret[0].(oidc.KeySet)
return ret0
}
// KeySet indicates an expected call of KeySet.
func (mr *MockIDTokenVerifierMockRecorder) KeySet() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockIDTokenVerifier)(nil).KeySet))
}
// MaxAge mocks base method.
func (m *MockIDTokenVerifier) MaxAge() time.Duration {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MaxAge")
ret0, _ := ret[0].(time.Duration)
return ret0
}
// MaxAge indicates an expected call of MaxAge.
func (mr *MockIDTokenVerifierMockRecorder) MaxAge() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxAge", reflect.TypeOf((*MockIDTokenVerifier)(nil).MaxAge))
}
// MaxAgeIAT mocks base method.
func (m *MockIDTokenVerifier) MaxAgeIAT() time.Duration {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MaxAgeIAT")
ret0, _ := ret[0].(time.Duration)
return ret0
}
// MaxAgeIAT indicates an expected call of MaxAgeIAT.
func (mr *MockIDTokenVerifierMockRecorder) MaxAgeIAT() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxAgeIAT", reflect.TypeOf((*MockIDTokenVerifier)(nil).MaxAgeIAT))
}
// Nonce mocks base method.
func (m *MockIDTokenVerifier) Nonce(arg0 context.Context) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Nonce", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// Nonce indicates an expected call of Nonce.
func (mr *MockIDTokenVerifierMockRecorder) Nonce(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Nonce", reflect.TypeOf((*MockIDTokenVerifier)(nil).Nonce), arg0)
}
// Offset mocks base method.
func (m *MockIDTokenVerifier) Offset() time.Duration {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Offset")
ret0, _ := ret[0].(time.Duration)
return ret0
}
// Offset indicates an expected call of Offset.
func (mr *MockIDTokenVerifierMockRecorder) Offset() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Offset", reflect.TypeOf((*MockIDTokenVerifier)(nil).Offset))
}
// SupportedSignAlgs mocks base method.
func (m *MockIDTokenVerifier) SupportedSignAlgs() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SupportedSignAlgs")
ret0, _ := ret[0].([]string)
return ret0
}
// SupportedSignAlgs indicates an expected call of SupportedSignAlgs.
func (mr *MockIDTokenVerifierMockRecorder) SupportedSignAlgs() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedSignAlgs", reflect.TypeOf((*MockIDTokenVerifier)(nil).SupportedSignAlgs))
}

View file

@ -4,20 +4,19 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"errors" "errors"
"log/slog" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
"github.com/go-jose/go-jose/v4"
"github.com/google/uuid" "github.com/google/uuid"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials" "gopkg.in/square/go-jose.v2"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" "github.com/zitadel/oidc/v2/pkg/client"
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/logging"
) )
const ( const (
@ -60,55 +59,38 @@ type RelyingParty interface {
// UserinfoEndpoint returns the userinfo // UserinfoEndpoint returns the userinfo
UserinfoEndpoint() string UserinfoEndpoint() string
// GetDeviceAuthorizationEndpoint returns the endpoint which can // GetDeviceAuthorizationEndpoint returns the enpoint which can
// be used to start a DeviceAuthorization flow. // be used to start a DeviceAuthorization flow.
GetDeviceAuthorizationEndpoint() string GetDeviceAuthorizationEndpoint() string
// IDTokenVerifier returns the verifier used for oidc id_token verification // IDTokenVerifier returns the verifier interface used for oidc id_token verification
IDTokenVerifier() *IDTokenVerifier IDTokenVerifier() IDTokenVerifier
// ErrorHandler returns the handler used for callback errors // ErrorHandler returns the handler used for callback errors
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
// Logger from the context, or a fallback if set.
Logger(context.Context) (logger *slog.Logger, ok bool)
}
type HasUnauthorizedHandler interface {
// UnauthorizedHandler returns the handler used for unauthorized errors
UnauthorizedHandler() func(w http.ResponseWriter, r *http.Request, desc string, state string)
} }
type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string)
type UnauthorizedHandler func(w http.ResponseWriter, r *http.Request, desc string, state string)
var DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { var DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError) http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError)
} }
var DefaultUnauthorizedHandler UnauthorizedHandler = func(w http.ResponseWriter, r *http.Request, desc string, state string) {
http.Error(w, desc, http.StatusUnauthorized)
}
type relyingParty struct { type relyingParty struct {
issuer string issuer string
DiscoveryEndpoint string DiscoveryEndpoint string
endpoints Endpoints endpoints Endpoints
oauthConfig *oauth2.Config oauthConfig *oauth2.Config
oauth2Only bool oauth2Only bool
pkce bool pkce bool
useSigningAlgsFromDiscovery bool
httpClient *http.Client httpClient *http.Client
cookieHandler *httphelper.CookieHandler cookieHandler *httphelper.CookieHandler
oauthAuthStyle oauth2.AuthStyle errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
idTokenVerifier IDTokenVerifier
errorHandler func(http.ResponseWriter, *http.Request, string, string, string) verifierOpts []VerifierOption
unauthorizedHandler func(http.ResponseWriter, *http.Request, string, string) signer jose.Signer
idTokenVerifier *IDTokenVerifier
verifierOpts []VerifierOption
signer jose.Signer
logger *slog.Logger
} }
func (rp *relyingParty) OAuthConfig() *oauth2.Config { func (rp *relyingParty) OAuthConfig() *oauth2.Config {
@ -155,7 +137,7 @@ func (rp *relyingParty) GetRevokeEndpoint() string {
return rp.endpoints.RevokeURL return rp.endpoints.RevokeURL
} }
func (rp *relyingParty) IDTokenVerifier() *IDTokenVerifier { func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier {
if rp.idTokenVerifier == nil { if rp.idTokenVerifier == nil {
rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...)
} }
@ -169,31 +151,14 @@ func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request,
return rp.errorHandler return rp.errorHandler
} }
func (rp *relyingParty) UnauthorizedHandler() func(http.ResponseWriter, *http.Request, string, string) {
if rp.unauthorizedHandler == nil {
rp.unauthorizedHandler = DefaultUnauthorizedHandler
}
return rp.unauthorizedHandler
}
func (rp *relyingParty) Logger(ctx context.Context) (logger *slog.Logger, ok bool) {
logger, ok = logging.FromContext(ctx)
if ok {
return logger, ok
}
return rp.logger, rp.logger != nil
}
// NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given // NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given
// OAuth2 Config and possible configOptions // OAuth2 Config and possible configOptions
// it will use the AuthURL and TokenURL set in config // it will use the AuthURL and TokenURL set in config
func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) { func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) {
rp := &relyingParty{ rp := &relyingParty{
oauthConfig: config, oauthConfig: config,
httpClient: httphelper.DefaultHTTPClient, httpClient: httphelper.DefaultHTTPClient,
oauth2Only: true, oauth2Only: true,
unauthorizedHandler: DefaultUnauthorizedHandler,
oauthAuthStyle: oauth2.AuthStyleAutoDetect,
} }
for _, optFunc := range options { for _, optFunc := range options {
@ -202,12 +167,9 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart
} }
} }
rp.oauthConfig.Endpoint.AuthStyle = rp.oauthAuthStyle
// avoid races by calling these early // avoid races by calling these early
_ = rp.IDTokenVerifier() // sets idTokenVerifier _ = rp.IDTokenVerifier() // sets idTokenVerifier
_ = rp.ErrorHandler() // sets errorHandler _ = rp.ErrorHandler() // sets errorHandler
_ = rp.UnauthorizedHandler() // sets unauthorizedHandler
return rp, nil return rp, nil
} }
@ -215,7 +177,7 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart
// NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given // NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given
// issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions // issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions
// it will run discovery on the provided issuer and use the found endpoints // it will run discovery on the provided issuer and use the found endpoints
func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) { func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) {
rp := &relyingParty{ rp := &relyingParty{
issuer: issuer, issuer: issuer,
oauthConfig: &oauth2.Config{ oauthConfig: &oauth2.Config{
@ -224,9 +186,8 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
RedirectURL: redirectURI, RedirectURL: redirectURI,
Scopes: scopes, Scopes: scopes,
}, },
httpClient: httphelper.DefaultHTTPClient, httpClient: httphelper.DefaultHTTPClient,
oauth2Only: false, oauth2Only: false,
oauthAuthStyle: oauth2.AuthStyleAutoDetect,
} }
for _, optFunc := range options { for _, optFunc := range options {
@ -234,25 +195,17 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
return nil, err return nil, err
} }
} }
ctx = logCtxWithRPData(ctx, rp, "function", "NewRelyingPartyOIDC") discoveryConfiguration, err := client.Discover(rp.issuer, rp.httpClient, rp.DiscoveryEndpoint)
discoveryConfiguration, err := client.Discover(ctx, rp.issuer, rp.httpClient, rp.DiscoveryEndpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rp.useSigningAlgsFromDiscovery {
rp.verifierOpts = append(rp.verifierOpts, WithSupportedSigningAlgorithms(discoveryConfiguration.IDTokenSigningAlgValuesSupported...))
}
endpoints := GetEndpoints(discoveryConfiguration) endpoints := GetEndpoints(discoveryConfiguration)
rp.oauthConfig.Endpoint = endpoints.Endpoint rp.oauthConfig.Endpoint = endpoints.Endpoint
rp.endpoints = endpoints rp.endpoints = endpoints
rp.oauthConfig.Endpoint.AuthStyle = rp.oauthAuthStyle
rp.endpoints.Endpoint.AuthStyle = rp.oauthAuthStyle
// avoid races by calling these early // avoid races by calling these early
_ = rp.IDTokenVerifier() // sets idTokenVerifier _ = rp.IDTokenVerifier() // sets idTokenVerifier
_ = rp.ErrorHandler() // sets errorHandler _ = rp.ErrorHandler() // sets errorHandler
_ = rp.UnauthorizedHandler() // sets unauthorizedHandler
return rp, nil return rp, nil
} }
@ -301,20 +254,6 @@ func WithErrorHandler(errorHandler ErrorHandler) Option {
} }
} }
func WithUnauthorizedHandler(unauthorizedHandler UnauthorizedHandler) Option {
return func(rp *relyingParty) error {
rp.unauthorizedHandler = unauthorizedHandler
return nil
}
}
func WithAuthStyle(oauthAuthStyle oauth2.AuthStyle) Option {
return func(rp *relyingParty) error {
rp.oauthAuthStyle = oauthAuthStyle
return nil
}
}
func WithVerifierOpts(opts ...VerifierOption) Option { func WithVerifierOpts(opts ...VerifierOption) Option {
return func(rp *relyingParty) error { return func(rp *relyingParty) error {
rp.verifierOpts = opts rp.verifierOpts = opts
@ -343,24 +282,6 @@ func WithJWTProfile(signerFromKey SignerFromKey) Option {
} }
} }
// WithLogger sets a logger that is used
// in case the request context does not contain a logger.
func WithLogger(logger *slog.Logger) Option {
return func(rp *relyingParty) error {
rp.logger = logger
return nil
}
}
// WithSigningAlgsFromDiscovery appends the [WithSupportedSigningAlgorithms] option to the Verifier Options.
// The algorithms returned in the `id_token_signing_alg_values_supported` from the discovery response will be set.
func WithSigningAlgsFromDiscovery() Option {
return func(rp *relyingParty) error {
rp.useSigningAlgsFromDiscovery = true
return nil
}
}
type SignerFromKey func() (jose.Signer, error) type SignerFromKey func() (jose.Signer, error)
func SignerFromKeyPath(path string) SignerFromKey { func SignerFromKeyPath(path string) SignerFromKey {
@ -389,6 +310,26 @@ func SignerFromKeyAndKeyID(key []byte, keyID string) SignerFromKey {
} }
} }
// Discover calls the discovery endpoint of the provided issuer and returns the found endpoints
//
// deprecated: use client.Discover
func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
req, err := http.NewRequest("GET", wellKnown, nil)
if err != nil {
return Endpoints{}, err
}
discoveryConfig := new(oidc.DiscoveryConfiguration)
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
if err != nil {
return Endpoints{}, err
}
if discoveryConfig.Issuer != issuer {
return Endpoints{}, oidc.ErrIssuerInvalid
}
return GetEndpoints(discoveryConfig), nil
}
// AuthURL returns the auth request url // AuthURL returns the auth request url
// (wrapping the oauth2 `AuthCodeURL`) // (wrapping the oauth2 `AuthCodeURL`)
func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string {
@ -400,29 +341,23 @@ func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string {
} }
// AuthURLHandler extends the `AuthURL` method with a http redirect handler // AuthURLHandler extends the `AuthURL` method with a http redirect handler
// including handling setting cookie for secure `state` transfer. // including handling setting cookie for secure `state` transfer
// Custom parameters can optionally be set to the redirect URL. func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc {
func AuthURLHandler(stateFn func() string, rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
opts := make([]AuthURLOpt, len(urlParam)) opts := make([]AuthURLOpt, 0)
for i, p := range urlParam {
opts[i] = AuthURLOpt(p)
}
state := stateFn() state := stateFn()
if err := trySetStateCookie(w, state, rp); err != nil { if err := trySetStateCookie(w, state, rp); err != nil {
unauthorizedError(w, r, "failed to create state cookie: "+err.Error(), state, rp) http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized)
return return
} }
if rp.IsPKCE() { if rp.IsPKCE() {
codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp) codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp)
if err != nil { if err != nil {
unauthorizedError(w, r, "failed to create code challenge: "+err.Error(), state, rp) http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized)
return return
} }
opts = append(opts, WithCodeChallenge(codeChallenge)) opts = append(opts, WithCodeChallenge(codeChallenge))
} }
http.Redirect(w, r, AuthURL(state, rp, opts...), http.StatusFound) http.Redirect(w, r, AuthURL(state, rp, opts...), http.StatusFound)
} }
} }
@ -436,173 +371,109 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri
return oidc.NewSHACodeChallenge(codeVerifier), nil return oidc.NewSHACodeChallenge(codeVerifier), nil
} }
// ErrMissingIDToken is returned when an id_token was expected,
// but not received in the token response.
var ErrMissingIDToken = errors.New("id_token missing")
func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Token, rp RelyingParty) (*oidc.Tokens[C], error) {
ctx, span := client.Tracer.Start(ctx, "verifyTokenResponse")
defer span.End()
if rp.IsOAuth2Only() {
return &oidc.Tokens[C]{Token: token}, nil
}
idTokenString, ok := token.Extra(idTokenKey).(string)
if !ok {
return &oidc.Tokens[C]{Token: token}, ErrMissingIDToken
}
idToken, err := VerifyTokens[C](ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
if err != nil {
return nil, err
}
return &oidc.Tokens[C]{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
}
// CodeExchange handles the oauth2 code exchange, extracting and validating the id_token // CodeExchange handles the oauth2 code exchange, extracting and validating the id_token
// returning it parsed together with the oauth2 tokens (access, refresh) // returning it parsed together with the oauth2 tokens (access, refresh)
func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) { func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) {
ctx, codeExchangeSpan := client.Tracer.Start(ctx, "CodeExchange")
defer codeExchangeSpan.End()
ctx = logCtxWithRPData(ctx, rp, "function", "CodeExchange")
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
codeOpts := make([]oauth2.AuthCodeOption, 0) codeOpts := make([]oauth2.AuthCodeOption, 0)
for _, opt := range opts { for _, opt := range opts {
codeOpts = append(codeOpts, opt()...) codeOpts = append(codeOpts, opt()...)
} }
ctx, oauthExchangeSpan := client.Tracer.Start(ctx, "OAuthExchange")
token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...) token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
oauthExchangeSpan.End()
return verifyTokenResponse[C](ctx, token, rp)
}
// ClientCredentials requests an access token using the `client_credentials` grant, if rp.IsOAuth2Only() {
// as defined in [RFC 6749, section 4.4]. return &oidc.Tokens{Token: token}, nil
//
// As there is no user associated to the request an ID Token can never be returned.
// Client Credentials are undefined in OpenID Connect and is a pure OAuth2 grant.
// Furthermore the server SHOULD NOT return a refresh token.
//
// [RFC 6749, section 4.4]: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
func ClientCredentials(ctx context.Context, rp RelyingParty, endpointParams url.Values) (token *oauth2.Token, err error) {
ctx = logCtxWithRPData(ctx, rp, "function", "ClientCredentials")
ctx, span := client.Tracer.Start(ctx, "ClientCredentials")
defer span.End()
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
config := clientcredentials.Config{
ClientID: rp.OAuthConfig().ClientID,
ClientSecret: rp.OAuthConfig().ClientSecret,
TokenURL: rp.OAuthConfig().Endpoint.TokenURL,
Scopes: rp.OAuthConfig().Scopes,
EndpointParams: endpointParams,
AuthStyle: rp.OAuthConfig().Endpoint.AuthStyle,
} }
return config.Token(ctx)
idTokenString, ok := token.Extra(idTokenKey).(string)
if !ok {
return nil, errors.New("id_token missing")
}
idToken, err := VerifyTokens(ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
if err != nil {
return nil, err
}
return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
} }
type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) type CodeExchangeCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty)
// CodeExchangeHandler extends the `CodeExchange` method with a http handler // CodeExchangeHandler extends the `CodeExchange` method with a http handler
// including cookie handling for secure `state` transfer // including cookie handling for secure `state` transfer
// and optional PKCE code verifier checking. // and optional PKCE code verifier checking
// Custom parameters can optionally be set to the token URL. func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.HandlerFunc {
func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx, span := client.Tracer.Start(r.Context(), "CodeExchangeHandler")
r = r.WithContext(ctx)
defer span.End()
state, err := tryReadStateCookie(w, r, rp) state, err := tryReadStateCookie(w, r, rp)
if err != nil { if err != nil {
unauthorizedError(w, r, "failed to get state: "+err.Error(), state, rp) http.Error(w, "failed to get state: "+err.Error(), http.StatusUnauthorized)
return return
} }
if errValue := r.FormValue("error"); errValue != "" { params := r.URL.Query()
rp.ErrorHandler()(w, r, errValue, r.FormValue("error_description"), state) if params.Get("error") != "" {
rp.ErrorHandler()(w, r, params.Get("error"), params.Get("error_description"), state)
return return
} }
codeOpts := make([]CodeExchangeOpt, len(urlParam)) codeOpts := make([]CodeExchangeOpt, 0)
for i, p := range urlParam {
codeOpts[i] = CodeExchangeOpt(p)
}
if rp.IsPKCE() { if rp.IsPKCE() {
codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode) codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode)
if err != nil { if err != nil {
unauthorizedError(w, r, "failed to get code verifier: "+err.Error(), state, rp) http.Error(w, "failed to get code verifier: "+err.Error(), http.StatusUnauthorized)
return return
} }
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
rp.CookieHandler().DeleteCookie(w, pkceCode)
} }
if rp.Signer() != nil { if rp.Signer() != nil {
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer(), rp.OAuthConfig().Endpoint.TokenURL}, time.Hour, rp.Signer()) assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer())
if err != nil { if err != nil {
unauthorizedError(w, r, "failed to build assertion: "+err.Error(), state, rp) http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized)
return return
} }
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
} }
tokens, err := CodeExchange[C](r.Context(), r.FormValue("code"), rp, codeOpts...) tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...)
if err != nil { if err != nil {
unauthorizedError(w, r, "failed to exchange token: "+err.Error(), state, rp) http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
return return
} }
callback(w, r, tokens, state, rp) callback(w, r, tokens, state, rp)
} }
} }
type SubjectGetter interface { type CodeExchangeUserinfoCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, provider RelyingParty, info oidc.UserInfo)
GetSubject() string
}
type CodeExchangeUserinfoCallback[C oidc.IDClaims, U SubjectGetter] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info U)
// UserinfoCallback wraps the callback function of the CodeExchangeHandler // UserinfoCallback wraps the callback function of the CodeExchangeHandler
// and calls the userinfo endpoint with the access token // and calls the userinfo endpoint with the access token
// on success it will pass the userinfo into its callback function as well // on success it will pass the userinfo into its callback function as well
func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCallback[C, U]) CodeExchangeCallback[C] { func UserinfoCallback(f CodeExchangeUserinfoCallback) CodeExchangeCallback {
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) { return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty) {
ctx, span := client.Tracer.Start(r.Context(), "UserinfoCallback") info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
r = r.WithContext(ctx)
defer span.End()
info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
if err != nil { if err != nil {
unauthorizedError(w, r, "userinfo failed: "+err.Error(), state, rp) http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized)
return return
} }
f(w, r, tokens, state, rp, info) f(w, r, tokens, state, rp, info)
} }
} }
// Userinfo will call the OIDC [UserInfo] Endpoint with the provided token and returns // Userinfo will call the OIDC Userinfo Endpoint with the provided token
// the response in an instance of type U. func Userinfo(token, tokenType, subject string, rp RelyingParty) (oidc.UserInfo, error) {
// [*oidc.UserInfo] can be used as a good example, or use a custom type if type-safe req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil)
// access to custom claims is needed.
//
// [UserInfo]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
func Userinfo[U SubjectGetter](ctx context.Context, token, tokenType, subject string, rp RelyingParty) (userinfo U, err error) {
var nilU U
ctx = logCtxWithRPData(ctx, rp, "function", "Userinfo")
ctx, span := client.Tracer.Start(ctx, "Userinfo")
defer span.End()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil)
if err != nil { if err != nil {
return nilU, err return nil, err
} }
req.Header.Set("authorization", tokenType+" "+token) req.Header.Set("authorization", tokenType+" "+token)
userinfo := oidc.NewUserInfo()
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil { if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
return nilU, err return nil, err
} }
if userinfo.GetSubject() != subject { if userinfo.GetSubject() != subject {
return nilU, ErrUserInfoSubNotMatching return nil, ErrUserInfoSubNotMatching
} }
return userinfo, nil return userinfo, nil
} }
@ -643,8 +514,9 @@ type Endpoints struct {
func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
return Endpoints{ return Endpoints{
Endpoint: oauth2.Endpoint{ Endpoint: oauth2.Endpoint{
AuthURL: discoveryConfig.AuthorizationEndpoint, AuthURL: discoveryConfig.AuthorizationEndpoint,
TokenURL: discoveryConfig.TokenEndpoint, AuthStyle: oauth2.AuthStyleAutoDetect,
TokenURL: discoveryConfig.TokenEndpoint,
}, },
IntrospectURL: discoveryConfig.IntrospectionEndpoint, IntrospectURL: discoveryConfig.IntrospectionEndpoint,
UserinfoURL: discoveryConfig.UserinfoEndpoint, UserinfoURL: discoveryConfig.UserinfoEndpoint,
@ -655,42 +527,6 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
} }
} }
// withURLParam sets custom url parameters.
// This is the generalized, unexported, function used by both
// URLParamOpt and AuthURLOpt.
func withURLParam(key, value string) func() []oauth2.AuthCodeOption {
return func() []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam(key, value),
}
}
}
// withPrompt sets the `prompt` params in the auth request
// This is the generalized, unexported, function used by both
// URLParamOpt and AuthURLOpt.
func withPrompt(prompt ...string) func() []oauth2.AuthCodeOption {
return withURLParam("prompt", oidc.SpaceDelimitedArray(prompt).String())
}
type URLParamOpt func() []oauth2.AuthCodeOption
// WithURLParam allows setting custom key-vale pairs
// to an OAuth2 URL.
func WithURLParam(key, value string) URLParamOpt {
return withURLParam(key, value)
}
// WithPromptURLParam sets the `prompt` parameter in a URL.
func WithPromptURLParam(prompt ...string) URLParamOpt {
return withPrompt(prompt...)
}
// WithResponseModeURLParam sets the `response_mode` parameter in a URL.
func WithResponseModeURLParam(mode oidc.ResponseMode) URLParamOpt {
return withURLParam("response_mode", string(mode))
}
type AuthURLOpt func() []oauth2.AuthCodeOption type AuthURLOpt func() []oauth2.AuthCodeOption
// WithCodeChallenge sets the `code_challenge` params in the auth request // WithCodeChallenge sets the `code_challenge` params in the auth request
@ -705,7 +541,11 @@ func WithCodeChallenge(codeChallenge string) AuthURLOpt {
// WithPrompt sets the `prompt` params in the auth request // WithPrompt sets the `prompt` params in the auth request
func WithPrompt(prompt ...string) AuthURLOpt { func WithPrompt(prompt ...string) AuthURLOpt {
return withPrompt(prompt...) return func() []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("prompt", oidc.SpaceDelimitedArray(prompt).Encode()),
}
}
} }
type CodeExchangeOpt func() []oauth2.AuthCodeOption type CodeExchangeOpt func() []oauth2.AuthCodeOption
@ -734,26 +574,15 @@ func (t tokenEndpointCaller) TokenEndpoint() string {
type RefreshTokenRequest struct { type RefreshTokenRequest struct {
RefreshToken string `schema:"refresh_token"` RefreshToken string `schema:"refresh_token"`
Scopes oidc.SpaceDelimitedArray `schema:"scope,omitempty"` Scopes oidc.SpaceDelimitedArray `schema:"scope"`
ClientID string `schema:"client_id,omitempty"` ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret,omitempty"` ClientSecret string `schema:"client_secret"`
ClientAssertion string `schema:"client_assertion,omitempty"` ClientAssertion string `schema:"client_assertion"`
ClientAssertionType string `schema:"client_assertion_type,omitempty"` ClientAssertionType string `schema:"client_assertion_type"`
GrantType oidc.GrantType `schema:"grant_type"` GrantType oidc.GrantType `schema:"grant_type"`
} }
// RefreshTokens performs a token refresh. If it doesn't error, it will always func RefreshAccessToken(rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oauth2.Token, error) {
// provide a new AccessToken. It may provide a new RefreshToken, and if it does, then
// the old one should be considered invalid.
//
// In case the RP is not OAuth2 only and an IDToken was part of the response,
// the IDToken and AccessToken will be verified
// and the IDToken and IDTokenClaims fields will be populated in the returned object.
func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oidc.Tokens[C], error) {
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
defer span.End()
ctx = logCtxWithRPData(ctx, rp, "function", "RefreshTokens")
request := RefreshTokenRequest{ request := RefreshTokenRequest{
RefreshToken: refreshToken, RefreshToken: refreshToken,
Scopes: rp.OAuthConfig().Scopes, Scopes: rp.OAuthConfig().Scopes,
@ -763,31 +592,17 @@ func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refres
ClientAssertionType: clientAssertionType, ClientAssertionType: clientAssertionType,
GrantType: oidc.GrantTypeRefreshToken, GrantType: oidc.GrantTypeRefreshToken,
} }
newToken, err := client.CallTokenEndpoint(ctx, request, tokenEndpointCaller{RelyingParty: rp}) return client.CallTokenEndpoint(request, tokenEndpointCaller{RelyingParty: rp})
if err != nil {
return nil, err
}
tokens, err := verifyTokenResponse[C](ctx, newToken, rp)
if err == nil || errors.Is(err, ErrMissingIDToken) {
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
// ...except that it might not contain an id_token.
return tokens, nil
}
return nil, err
} }
func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) { func EndSession(rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) {
ctx = logCtxWithRPData(ctx, rp, "function", "EndSession")
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
defer span.End()
request := oidc.EndSessionRequest{ request := oidc.EndSessionRequest{
IdTokenHint: idToken, IdTokenHint: idToken,
ClientID: rp.OAuthConfig().ClientID, ClientID: rp.OAuthConfig().ClientID,
PostLogoutRedirectURI: optionalRedirectURI, PostLogoutRedirectURI: optionalRedirectURI,
State: optionalState, State: optionalState,
} }
return client.CallEndSessionEndpoint(ctx, request, nil, rp) return client.CallEndSessionEndpoint(request, nil, rp)
} }
// RevokeToken requires a RelyingParty that is also a client.RevokeCaller. The RelyingParty // RevokeToken requires a RelyingParty that is also a client.RevokeCaller. The RelyingParty
@ -795,10 +610,7 @@ func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectU
// NewRelyingPartyOAuth() does not. // NewRelyingPartyOAuth() does not.
// //
// tokenTypeHint should be either "id_token" or "refresh_token". // tokenTypeHint should be either "id_token" or "refresh_token".
func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHint string) error { func RevokeToken(rp RelyingParty, token string, tokenTypeHint string) error {
ctx = logCtxWithRPData(ctx, rp, "function", "RevokeToken")
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
defer span.End()
request := client.RevokeRequest{ request := client.RevokeRequest{
Token: token, Token: token,
TokenTypeHint: tokenTypeHint, TokenTypeHint: tokenTypeHint,
@ -806,15 +618,7 @@ func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHi
ClientSecret: rp.OAuthConfig().ClientSecret, ClientSecret: rp.OAuthConfig().ClientSecret,
} }
if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" { if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" {
return client.CallRevokeEndpoint(ctx, request, nil, rc) return client.CallRevokeEndpoint(request, nil, rc)
} }
return ErrRelyingPartyNotSupportRevokeCaller return fmt.Errorf("RelyingParty does not support RevokeCaller")
}
func unauthorizedError(w http.ResponseWriter, r *http.Request, desc string, state string, rp RelyingParty) {
if rp, ok := rp.(HasUnauthorizedHandler); ok {
rp.UnauthorizedHandler()(w, r, desc, state)
return
}
http.Error(w, desc, http.StatusUnauthorized)
} }

View file

@ -1,107 +0,0 @@
package rp
import (
"context"
"testing"
"time"
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
)
func Test_verifyTokenResponse(t *testing.T) {
verifier := &IDTokenVerifier{
Issuer: tu.ValidIssuer,
MaxAgeIAT: 2 * time.Minute,
ClientID: tu.ValidClientID,
Offset: time.Second,
SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
KeySet: tu.KeySet{},
MaxAge: 2 * time.Minute,
ACR: tu.ACRVerify,
Nonce: func(context.Context) string { return tu.ValidNonce },
}
tests := []struct {
name string
oauth2Only bool
tokens func() (token *oauth2.Token, want *oidc.Tokens[*oidc.IDTokenClaims])
wantErr error
}{
{
name: "succes, oauth2 only",
oauth2Only: true,
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
accesToken, _ := tu.ValidAccessToken()
token := &oauth2.Token{
AccessToken: accesToken,
}
return token, &oidc.Tokens[*oidc.IDTokenClaims]{
Token: token,
}
},
},
{
name: "id_token missing error",
oauth2Only: false,
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
accesToken, _ := tu.ValidAccessToken()
token := &oauth2.Token{
AccessToken: accesToken,
}
return token, &oidc.Tokens[*oidc.IDTokenClaims]{
Token: token,
}
},
wantErr: ErrMissingIDToken,
},
{
name: "verify tokens error",
oauth2Only: false,
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
accesToken, _ := tu.ValidAccessToken()
token := &oauth2.Token{
AccessToken: accesToken,
}
token = token.WithExtra(map[string]any{
"id_token": "foobar",
})
return token, nil
},
wantErr: oidc.ErrParse,
},
{
name: "success, with id_token",
oauth2Only: false,
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
accesToken, _ := tu.ValidAccessToken()
token := &oauth2.Token{
AccessToken: accesToken,
}
idToken, claims := tu.ValidIDToken()
token = token.WithExtra(map[string]any{
"id_token": idToken,
})
return token, &oidc.Tokens[*oidc.IDTokenClaims]{
Token: token,
IDTokenClaims: claims,
IDToken: idToken,
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rp := &relyingParty{
oauth2Only: tt.oauth2Only,
idTokenVerifier: verifier,
}
token, want := tt.tokens()
got, err := verifyTokenResponse[*oidc.IDTokenClaims](context.Background(), token, rp)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, want, got)
})
}
}

View file

@ -5,7 +5,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc/grants/tokenexchange" "github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
) )
// TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` // TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`

View file

@ -1,45 +0,0 @@
package rp_test
import (
"context"
"fmt"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
)
type UserInfo struct {
Subject string `json:"sub,omitempty"`
oidc.UserInfoProfile
oidc.UserInfoEmail
oidc.UserInfoPhone
Address *oidc.UserInfoAddress `json:"address,omitempty"`
// Foo and Bar are custom claims
Foo string `json:"foo,omitempty"`
Bar struct {
Val1 string `json:"val_1,omitempty"`
Val2 string `json:"val_2,omitempty"`
} `json:"bar,omitempty"`
// Claims are all the combined claims, including custom.
Claims map[string]any `json:"-,omitempty"`
}
func (u *UserInfo) GetSubject() string {
return u.Subject
}
func ExampleUserinfo_custom() {
rpo, err := rp.NewRelyingPartyOIDC(context.TODO(), "http://localhost:8080", "clientid", "clientsecret", "http://example.com/redirect", []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone})
if err != nil {
panic(err)
}
info, err := rp.Userinfo[*UserInfo](context.TODO(), "accesstokenstring", "Bearer", "userid", rpo)
if err != nil {
panic(err)
}
fmt.Println(info)
}

View file

@ -4,93 +4,90 @@ import (
"context" "context"
"time" "time"
jose "github.com/go-jose/go-jose/v4" "gopkg.in/square/go-jose.v2"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" "github.com/zitadel/oidc/v2/pkg/oidc"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
type IDTokenVerifier interface {
oidc.Verifier
ClientID() string
SupportedSignAlgs() []string
KeySet() oidc.KeySet
Nonce(context.Context) string
ACR() oidc.ACRVerifier
MaxAge() time.Duration
}
// VerifyTokens implement the Token Response Validation as defined in OIDC specification // VerifyTokens implement the Token Response Validation as defined in OIDC specification
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v *IDTokenVerifier) (claims C, err error) { func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (oidc.IDTokenClaims, error) {
ctx, span := client.Tracer.Start(ctx, "VerifyTokens") idToken, err := VerifyIDToken(ctx, idTokenString, v)
defer span.End()
var nilClaims C
claims, err = VerifyIDToken[C](ctx, idToken, v)
if err != nil { if err != nil {
return nilClaims, err return nil, err
} }
if err := VerifyAccessToken(accessToken, claims.GetAccessTokenHash(), claims.GetSignatureAlgorithm()); err != nil { if err := VerifyAccessToken(accessToken, idToken.GetAccessTokenHash(), idToken.GetSignatureAlgorithm()); err != nil {
return nilClaims, err return nil, err
} }
return claims, nil return idToken, nil
} }
// VerifyIDToken validates the id token according to // VerifyIDToken validates the id token according to
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenVerifier) (claims C, err error) { func VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (oidc.IDTokenClaims, error) {
ctx, span := client.Tracer.Start(ctx, "VerifyIDToken") claims := oidc.EmptyIDTokenClaims()
defer span.End()
var nilClaims C
decrypted, err := oidc.DecryptToken(token) decrypted, err := oidc.DecryptToken(token)
if err != nil { if err != nil {
return nilClaims, err return nil, err
} }
payload, err := oidc.ParseToken(decrypted, &claims) payload, err := oidc.ParseToken(decrypted, claims)
if err != nil { if err != nil {
return nilClaims, err return nil, err
} }
if err := oidc.CheckSubject(claims); err != nil { if err := oidc.CheckSubject(claims); err != nil {
return nilClaims, err return nil, err
} }
if err = oidc.CheckIssuer(claims, v.Issuer); err != nil { if err = oidc.CheckIssuer(claims, v.Issuer()); err != nil {
return nilClaims, err return nil, err
} }
if err = oidc.CheckAudience(claims, v.ClientID); err != nil { if err = oidc.CheckAudience(claims, v.ClientID()); err != nil {
return nilClaims, err return nil, err
} }
if err = oidc.CheckAuthorizedParty(claims, v.ClientID); err != nil { if err = oidc.CheckAuthorizedParty(claims, v.ClientID()); err != nil {
return nilClaims, err return nil, err
} }
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs, v.KeySet); err != nil { if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
return nilClaims, err return nil, err
} }
if err = oidc.CheckExpiration(claims, v.Offset); err != nil { if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
return nilClaims, err return nil, err
} }
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil { if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil {
return nilClaims, err return nil, err
} }
if v.Nonce != nil { if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil { return nil, err
return nilClaims, err
}
} }
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil { if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil {
return nilClaims, err return nil, err
} }
if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil { if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil {
return nilClaims, err return nil, err
} }
return claims, nil return claims, nil
} }
type IDTokenVerifier oidc.Verifier
// VerifyAccessToken validates the access token according to // VerifyAccessToken validates the access token according to
// https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation // https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error { func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
@ -108,14 +105,15 @@ func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAl
return nil return nil
} }
// NewIDTokenVerifier returns a oidc.Verifier suitable for ID token verification. // NewIDTokenVerifier returns an implementation of `IDTokenVerifier`
func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) *IDTokenVerifier { // for `VerifyTokens` and `VerifyIDToken`
v := &IDTokenVerifier{ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) IDTokenVerifier {
Issuer: issuer, v := &idTokenVerifier{
ClientID: clientID, issuer: issuer,
KeySet: keySet, clientID: clientID,
Offset: time.Second, keySet: keySet,
Nonce: func(_ context.Context) string { offset: 1 * time.Second,
nonce: func(_ context.Context) string {
return "" return ""
}, },
} }
@ -128,47 +126,95 @@ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...
} }
// VerifierOption is the type for providing dynamic options to the IDTokenVerifier // VerifierOption is the type for providing dynamic options to the IDTokenVerifier
type VerifierOption func(*IDTokenVerifier) type VerifierOption func(*idTokenVerifier)
// WithIssuedAtOffset mitigates the risk of iat to be in the future // WithIssuedAtOffset mitigates the risk of iat to be in the future
// because of clock skews with the ability to add an offset to the current time // because of clock skews with the ability to add an offset to the current time
func WithIssuedAtOffset(offset time.Duration) VerifierOption { func WithIssuedAtOffset(offset time.Duration) func(*idTokenVerifier) {
return func(v *IDTokenVerifier) { return func(v *idTokenVerifier) {
v.Offset = offset v.offset = offset
} }
} }
// WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now // WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
func WithIssuedAtMaxAge(maxAge time.Duration) VerifierOption { func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) {
return func(v *IDTokenVerifier) { return func(v *idTokenVerifier) {
v.MaxAgeIAT = maxAge v.maxAge = maxAge
} }
} }
// WithNonce sets the function to check the nonce // WithNonce sets the function to check the nonce
func WithNonce(nonce func(context.Context) string) VerifierOption { func WithNonce(nonce func(context.Context) string) VerifierOption {
return func(v *IDTokenVerifier) { return func(v *idTokenVerifier) {
v.Nonce = nonce v.nonce = nonce
} }
} }
// WithACRVerifier sets the verifier for the acr claim // WithACRVerifier sets the verifier for the acr claim
func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption { func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption {
return func(v *IDTokenVerifier) { return func(v *idTokenVerifier) {
v.ACR = verifier v.acr = verifier
} }
} }
// WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now // WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now
func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption { func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption {
return func(v *IDTokenVerifier) { return func(v *idTokenVerifier) {
v.MaxAge = maxAge v.maxAge = maxAge
} }
} }
// WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm // WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm
func WithSupportedSigningAlgorithms(algs ...string) VerifierOption { func WithSupportedSigningAlgorithms(algs ...string) VerifierOption {
return func(v *IDTokenVerifier) { return func(v *idTokenVerifier) {
v.SupportedSignAlgs = algs v.supportedSignAlgs = algs
} }
} }
type idTokenVerifier struct {
issuer string
maxAgeIAT time.Duration
offset time.Duration
clientID string
supportedSignAlgs []string
keySet oidc.KeySet
acr oidc.ACRVerifier
maxAge time.Duration
nonce func(ctx context.Context) string
}
func (i *idTokenVerifier) Issuer() string {
return i.issuer
}
func (i *idTokenVerifier) MaxAgeIAT() time.Duration {
return i.maxAgeIAT
}
func (i *idTokenVerifier) Offset() time.Duration {
return i.offset
}
func (i *idTokenVerifier) ClientID() string {
return i.clientID
}
func (i *idTokenVerifier) SupportedSignAlgs() []string {
return i.supportedSignAlgs
}
func (i *idTokenVerifier) KeySet() oidc.KeySet {
return i.keySet
}
func (i *idTokenVerifier) Nonce(ctx context.Context) string {
return i.nonce(ctx)
}
func (i *idTokenVerifier) ACR() oidc.ACRVerifier {
return i.acr
}
func (i *idTokenVerifier) MaxAge() time.Duration {
return i.maxAge
}

View file

@ -1,359 +0,0 @@
package rp
import (
"context"
"testing"
"time"
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
jose "github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVerifyTokens(t *testing.T) {
verifier := &IDTokenVerifier{
Issuer: tu.ValidIssuer,
MaxAgeIAT: 2 * time.Minute,
Offset: time.Second,
SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
KeySet: tu.KeySet{},
MaxAge: 2 * time.Minute,
ACR: tu.ACRVerify,
Nonce: func(context.Context) string { return tu.ValidNonce },
ClientID: tu.ValidClientID,
}
accessToken, _ := tu.ValidAccessToken()
atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm)
require.NoError(t, err)
tests := []struct {
name string
accessToken string
idTokenClaims func() (string, *oidc.IDTokenClaims)
wantErr bool
}{
{
name: "without access token",
idTokenClaims: tu.ValidIDToken,
},
{
name: "with access token",
accessToken: accessToken,
idTokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, atHash,
)
},
},
{
name: "expired id token",
accessToken: accessToken,
idTokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, atHash,
)
},
wantErr: true,
},
{
name: "wrong access token",
accessToken: accessToken,
idTokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "~~~",
)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
idToken, want := tt.idTokenClaims()
got, err := VerifyTokens[*oidc.IDTokenClaims](context.Background(), tt.accessToken, idToken, verifier)
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
return
}
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, got, want)
})
}
}
func TestVerifyIDToken(t *testing.T) {
verifier := &IDTokenVerifier{
Issuer: tu.ValidIssuer,
MaxAgeIAT: 2 * time.Minute,
Offset: time.Second,
SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
KeySet: tu.KeySet{},
MaxAge: 2 * time.Minute,
ACR: tu.ACRVerify,
Nonce: func(context.Context) string { return tu.ValidNonce },
ClientID: tu.ValidClientID,
}
tests := []struct {
name string
tokenClaims func() (string, *oidc.IDTokenClaims)
customVerifier func(verifier *IDTokenVerifier)
wantErr bool
}{
{
name: "success",
tokenClaims: tu.ValidIDToken,
},
{
name: "custom claims",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDTokenCustom(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
map[string]any{"some": "thing"},
)
},
},
{
name: "skip nonce check",
customVerifier: func(verifier *IDTokenVerifier) {
verifier.Nonce = nil
},
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, "foo",
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
},
{
name: "parse err",
tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil },
wantErr: true,
},
{
name: "invalid signature",
tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil },
wantErr: true,
},
{
name: "empty subject",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, "", tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
{
name: "wrong issuer",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
"foo", tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
{
name: "wrong clientID",
customVerifier: func(verifier *IDTokenVerifier) {
verifier.ClientID = "foo"
},
tokenClaims: tu.ValidIDToken,
wantErr: true,
},
{
name: "expired",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
{
name: "wrong IAT",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, -time.Hour, "",
)
},
wantErr: true,
},
{
name: "wrong acr",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
"else", tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
{
name: "expired auth",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime.Add(-time.Hour), tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
{
name: "wrong nonce",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, "foo",
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
token, want := tt.tokenClaims()
if tt.customVerifier != nil {
tt.customVerifier(verifier)
}
got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier)
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
return
}
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, got, want)
})
}
}
func TestVerifyAccessToken(t *testing.T) {
token, _ := tu.ValidAccessToken()
hash, err := oidc.ClaimHash(token, tu.SignatureAlgorithm)
require.NoError(t, err)
type args struct {
accessToken string
atHash string
sigAlgorithm jose.SignatureAlgorithm
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty hash",
},
{
name: "success",
args: args{
accessToken: token,
atHash: hash,
sigAlgorithm: tu.SignatureAlgorithm,
},
},
{
name: "invalid algorithm",
args: args{
accessToken: token,
atHash: hash,
sigAlgorithm: "foo",
},
wantErr: true,
},
{
name: "mismatch",
args: args{
accessToken: token,
atHash: "~~",
sigAlgorithm: tu.SignatureAlgorithm,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := VerifyAccessToken(tt.args.accessToken, tt.args.atHash, tt.args.sigAlgorithm)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestNewIDTokenVerifier(t *testing.T) {
type args struct {
issuer string
clientID string
keySet oidc.KeySet
options []VerifierOption
}
tests := []struct {
name string
args args
want *IDTokenVerifier
}{
{
name: "nil nonce", // otherwise assert.Equal will fail on the function
args: args{
issuer: tu.ValidIssuer,
clientID: tu.ValidClientID,
keySet: tu.KeySet{},
options: []VerifierOption{
WithIssuedAtOffset(time.Minute),
WithIssuedAtMaxAge(time.Hour),
WithNonce(nil), // otherwise assert.Equal will fail on the function
WithACRVerifier(nil),
WithAuthTimeMaxAge(2 * time.Hour),
WithSupportedSigningAlgorithms("ABC", "DEF"),
},
},
want: &IDTokenVerifier{
Issuer: tu.ValidIssuer,
Offset: time.Minute,
MaxAgeIAT: time.Hour,
ClientID: tu.ValidClientID,
KeySet: tu.KeySet{},
Nonce: nil,
ACR: nil,
MaxAge: 2 * time.Hour,
SupportedSignAlgs: []string{"ABC", "DEF"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewIDTokenVerifier(tt.args.issuer, tt.args.clientID, tt.args.keySet, tt.args.options...)
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -1,86 +0,0 @@
package rp_test
import (
"context"
"fmt"
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
)
// MyCustomClaims extends the TokenClaims base,
// so it implmeents the oidc.Claims interface.
// Instead of carrying a map, we add needed fields// to the struct for type safe access.
type MyCustomClaims struct {
oidc.TokenClaims
NotBefore oidc.Time `json:"nbf,omitempty"`
AccessTokenHash string `json:"at_hash,omitempty"`
Foo string `json:"foo,omitempty"`
Bar *Nested `json:"bar,omitempty"`
}
// GetAccessTokenHash is required to implement
// the oidc.IDClaims interface.
func (c *MyCustomClaims) GetAccessTokenHash() string {
return c.AccessTokenHash
}
// Nested struct types are also possible.
type Nested struct {
Count int `json:"count,omitempty"`
Tags []string `json:"tags,omitempty"`
}
/*
idToken carries the following claims. foo and bar are custom claims
{
"acr": "something",
"amr": [
"foo",
"bar"
],
"at_hash": "2dzbm_vIxy-7eRtqUIGPPw",
"aud": [
"unit",
"test",
"555666"
],
"auth_time": 1678100961,
"azp": "555666",
"bar": {
"count": 22,
"tags": [
"some",
"tags"
]
},
"client_id": "555666",
"exp": 4802238682,
"foo": "Hello, World!",
"iat": 1678101021,
"iss": "local.com",
"jti": "9876",
"nbf": 1678101021,
"nonce": "12345",
"sub": "tim@local.com"
}
*/
const idToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhY3IiOiJzb21ldGhpbmciLCJhbXIiOlsiZm9vIiwiYmFyIl0sImF0X2hhc2giOiIyZHpibV92SXh5LTdlUnRxVUlHUFB3IiwiYXVkIjpbInVuaXQiLCJ0ZXN0IiwiNTU1NjY2Il0sImF1dGhfdGltZSI6MTY3ODEwMDk2MSwiYXpwIjoiNTU1NjY2IiwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiY2xpZW50X2lkIjoiNTU1NjY2IiwiZXhwIjo0ODAyMjM4NjgyLCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MTAxMDIxLCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MTAxMDIxLCJub25jZSI6IjEyMzQ1Iiwic3ViIjoidGltQGxvY2FsLmNvbSJ9.t3GXSfVNNwiW1Suv9_84v0sdn2_-RWHVxhphhRozDXnsO7SDNOlGnEioemXABESxSzMclM7gB7mYy5Qah2ZUNx7eP5t2njoxEYfavgHwx7UJZ2NCg8NDPQyr-hlxelEcfdXK-I0oTd-FRDvF4rqPkD9Us52IpnplChCxnHFgh4wKwPqZZjv2IXVCtn0ilKW3hff1rMOYKEuLRcN2YP0gkyuqyHvcf2dMmjod0t4sLOTJ82rsCbMBC5CLpqv3nIC9HOGITkt1Kd-Am0n1LrdZvWwTo6RFe8AnzF0gpqjcB5Wg4Qeh58DIjZOz4f_8wnmJ_gCqyRh5vfSW4XHdbum0Tw`
const accessToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOlsidW5pdCIsInRlc3QiXSwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiZXhwIjo0ODAyMjM4NjgyLCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MTAxMDIxLCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MTAxMDIxLCJzdWIiOiJ0aW1AbG9jYWwuY29tIn0.Zrz3LWSRjCMJZUMaI5dUbW4vGdSmEeJQ3ouhaX0bcW9rdFFLgBI4K2FWJhNivq8JDmCGSxwLu3mI680GWmDaEoAx1M5sCO9lqfIZHGZh-lfAXk27e6FPLlkTDBq8Bx4o4DJ9Fw0hRJGjUTjnYv5cq1vo2-UqldasL6CwTbkzNC_4oQFfRtuodC4Ql7dZ1HRv5LXuYx7KPkOssLZtV9cwtJp5nFzKjcf2zEE_tlbjcpynMwypornRUp1EhCWKRUGkJhJeiP71ECY5pQhShfjBu9Nc5wDpSnZmnk2S4YsPrRK3QkE-iEkas8BfsOCrGoErHjEJexAIDjasGO5PFLWfCA`
func ExampleVerifyTokens_customClaims() {
v := rp.NewIDTokenVerifier("local.com", "555666", tu.KeySet{},
rp.WithNonce(func(ctx context.Context) string { return "12345" }),
)
// VerifyAccessToken can be called with the *MyCustomClaims.
claims, err := rp.VerifyTokens[*MyCustomClaims](context.TODO(), accessToken, idToken, v)
if err != nil {
panic(err)
}
// Here we have typesafe access to the custom claims
fmt.Println(claims.Foo, claims.Bar.Count, claims.Bar.Tags)
// Output: Hello, World! 22 [some tags]
}

View file

@ -1,52 +0,0 @@
package rs_test
import (
"context"
"fmt"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
)
type IntrospectionResponse struct {
Active bool `json:"active"`
Scope oidc.SpaceDelimitedArray `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
TokenType string `json:"token_type,omitempty"`
Expiration oidc.Time `json:"exp,omitempty"`
IssuedAt oidc.Time `json:"iat,omitempty"`
NotBefore oidc.Time `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
Audience oidc.Audience `json:"aud,omitempty"`
Issuer string `json:"iss,omitempty"`
JWTID string `json:"jti,omitempty"`
Username string `json:"username,omitempty"`
oidc.UserInfoProfile
oidc.UserInfoEmail
oidc.UserInfoPhone
Address *oidc.UserInfoAddress `json:"address,omitempty"`
// Foo and Bar are custom claims
Foo string `json:"foo,omitempty"`
Bar struct {
Val1 string `json:"val_1,omitempty"`
Val2 string `json:"val_2,omitempty"`
} `json:"bar,omitempty"`
// Claims are all the combined claims, including custom.
Claims map[string]any `json:"-,omitempty"`
}
func ExampleIntrospect_custom() {
rss, err := rs.NewResourceServerClientCredentials(context.TODO(), "http://localhost:8080", "clientid", "clientsecret")
if err != nil {
panic(err)
}
resp, err := rs.Introspect[*IntrospectionResponse](context.TODO(), rss, "accesstokenstring")
if err != nil {
panic(err)
}
fmt.Println(resp)
}

View file

@ -6,16 +6,16 @@ import (
"net/http" "net/http"
"time" "time"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" "github.com/zitadel/oidc/v2/pkg/client"
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type ResourceServer interface { type ResourceServer interface {
IntrospectionURL() string IntrospectionURL() string
TokenEndpoint() string TokenEndpoint() string
HttpClient() *http.Client HttpClient() *http.Client
AuthFn() (any, error) AuthFn() (interface{}, error)
} }
type resourceServer struct { type resourceServer struct {
@ -23,7 +23,7 @@ type resourceServer struct {
tokenURL string tokenURL string
introspectURL string introspectURL string
httpClient *http.Client httpClient *http.Client
authFn func() (any, error) authFn func() (interface{}, error)
} }
func (r *resourceServer) IntrospectionURL() string { func (r *resourceServer) IntrospectionURL() string {
@ -38,33 +38,33 @@ func (r *resourceServer) HttpClient() *http.Client {
return r.httpClient return r.httpClient
} }
func (r *resourceServer) AuthFn() (any, error) { func (r *resourceServer) AuthFn() (interface{}, error) {
return r.authFn() return r.authFn()
} }
func NewResourceServerClientCredentials(ctx context.Context, issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) { func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) {
authorizer := func() (any, error) { authorizer := func() (interface{}, error) {
return httphelper.AuthorizeBasic(clientID, clientSecret), nil return httphelper.AuthorizeBasic(clientID, clientSecret), nil
} }
return newResourceServer(ctx, issuer, authorizer, option...) return newResourceServer(issuer, authorizer, option...)
} }
func NewResourceServerJWTProfile(ctx context.Context, issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) { func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) {
signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) signer, err := client.NewSignerFromPrivateKeyByte(key, keyID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
authorizer := func() (any, error) { authorizer := func() (interface{}, error) {
assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer) assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return client.ClientAssertionFormAuthorization(assertion), nil return client.ClientAssertionFormAuthorization(assertion), nil
} }
return newResourceServer(ctx, issuer, authorizer, options...) return newResourceServer(issuer, authorizer, options...)
} }
func newResourceServer(ctx context.Context, issuer string, authorizer func() (any, error), options ...Option) (*resourceServer, error) { func newResourceServer(issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) {
rs := &resourceServer{ rs := &resourceServer{
issuer: issuer, issuer: issuer,
httpClient: httphelper.DefaultHTTPClient, httpClient: httphelper.DefaultHTTPClient,
@ -73,30 +73,26 @@ func newResourceServer(ctx context.Context, issuer string, authorizer func() (an
optFunc(rs) optFunc(rs)
} }
if rs.introspectURL == "" || rs.tokenURL == "" { if rs.introspectURL == "" || rs.tokenURL == "" {
config, err := client.Discover(ctx, rs.issuer, rs.httpClient) config, err := client.Discover(rs.issuer, rs.httpClient)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rs.tokenURL == "" { rs.tokenURL = config.TokenEndpoint
rs.tokenURL = config.TokenEndpoint rs.introspectURL = config.IntrospectionEndpoint
}
if rs.introspectURL == "" {
rs.introspectURL = config.IntrospectionEndpoint
}
} }
if rs.tokenURL == "" { if rs.introspectURL == "" || rs.tokenURL == "" {
return nil, errors.New("tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url")
} }
rs.authFn = authorizer rs.authFn = authorizer
return rs, nil return rs, nil
} }
func NewResourceServerFromKeyFile(ctx context.Context, issuer, path string, options ...Option) (ResourceServer, error) { func NewResourceServerFromKeyFile(issuer, path string, options ...Option) (ResourceServer, error) {
c, err := client.ConfigFromKeyFile(path) c, err := client.ConfigFromKeyFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewResourceServerJWTProfile(ctx, issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) return NewResourceServerJWTProfile(issuer, c.ClientID, c.KeyID, []byte(c.Key), options...)
} }
type Option func(*resourceServer) type Option func(*resourceServer)
@ -116,30 +112,18 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option {
} }
} }
// Introspect calls the [RFC7662] Token Introspection func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) {
// endpoint and returns the response in an instance of type R.
// [*oidc.IntrospectionResponse] can be used as a good example, or use a custom type if type-safe
// access to custom claims is needed.
//
// [RFC7662]: https://www.rfc-editor.org/rfc/rfc7662
func Introspect[R any](ctx context.Context, rp ResourceServer, token string) (resp R, err error) {
ctx, span := client.Tracer.Start(ctx, "Introspect")
defer span.End()
if rp.IntrospectionURL() == "" {
return resp, errors.New("resource server: introspection URL is empty")
}
authFn, err := rp.AuthFn() authFn, err := rp.AuthFn()
if err != nil { if err != nil {
return resp, err return nil, err
} }
req, err := httphelper.FormRequest(ctx, rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn) req, err := httphelper.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn)
if err != nil { if err != nil {
return resp, err return nil, err
} }
resp := oidc.NewIntrospectionResponse()
if err := httphelper.HttpRequest(rp.HttpClient(), req, &resp); err != nil { if err := httphelper.HttpRequest(rp.HttpClient(), req, resp); err != nil {
return resp, err return nil, err
} }
return resp, nil return resp, nil
} }

View file

@ -1,221 +0,0 @@
package rs
import (
"context"
"testing"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewResourceServer(t *testing.T) {
type args struct {
issuer string
authorizer func() (any, error)
options []Option
}
type wantFields struct {
issuer string
tokenURL string
introspectURL string
authFn func() (any, error)
}
tests := []struct {
name string
args args
wantFields *wantFields
wantErr bool
}{
{
name: "spotify-full-discovery",
args: args{
issuer: "https://accounts.spotify.com",
authorizer: nil,
options: []Option{},
},
wantFields: &wantFields{
issuer: "https://accounts.spotify.com",
tokenURL: "https://accounts.spotify.com/api/token",
introspectURL: "",
authFn: nil,
},
wantErr: false,
},
{
name: "spotify-with-static-tokenurl",
args: args{
issuer: "https://accounts.spotify.com",
authorizer: nil,
options: []Option{
WithStaticEndpoints(
"https://some.host/token-url",
"",
),
},
},
wantFields: &wantFields{
issuer: "https://accounts.spotify.com",
tokenURL: "https://some.host/token-url",
introspectURL: "",
authFn: nil,
},
wantErr: false,
},
{
name: "spotify-with-static-introspecturl",
args: args{
issuer: "https://accounts.spotify.com",
authorizer: nil,
options: []Option{
WithStaticEndpoints(
"",
"https://some.host/instrospect-url",
),
},
},
wantFields: &wantFields{
issuer: "https://accounts.spotify.com",
tokenURL: "https://accounts.spotify.com/api/token",
introspectURL: "https://some.host/instrospect-url",
authFn: nil,
},
wantErr: false,
},
{
name: "spotify-with-all-static-endpoints",
args: args{
issuer: "https://accounts.spotify.com",
authorizer: nil,
options: []Option{
WithStaticEndpoints(
"https://some.host/token-url",
"https://some.host/instrospect-url",
),
},
},
wantFields: &wantFields{
issuer: "https://accounts.spotify.com",
tokenURL: "https://some.host/token-url",
introspectURL: "https://some.host/instrospect-url",
authFn: nil,
},
wantErr: false,
},
{
name: "bad-discovery",
args: args{
issuer: "https://127.0.0.1:65535",
authorizer: nil,
options: []Option{},
},
wantFields: nil,
wantErr: true,
},
{
name: "bad-discovery-with-static-tokenurl",
args: args{
issuer: "https://127.0.0.1:65535",
authorizer: nil,
options: []Option{
WithStaticEndpoints(
"https://some.host/token-url",
"",
),
},
},
wantFields: nil,
wantErr: true,
},
{
name: "bad-discovery-with-static-introspecturl",
args: args{
issuer: "https://127.0.0.1:65535",
authorizer: nil,
options: []Option{
WithStaticEndpoints(
"",
"https://some.host/instrospect-url",
),
},
},
wantFields: nil,
wantErr: true,
},
{
name: "bad-discovery-with-all-static-endpoints",
args: args{
issuer: "https://127.0.0.1:65535",
authorizer: nil,
options: []Option{
WithStaticEndpoints(
"https://some.host/token-url",
"https://some.host/instrospect-url",
),
},
},
wantFields: &wantFields{
issuer: "https://127.0.0.1:65535",
tokenURL: "https://some.host/token-url",
introspectURL: "https://some.host/instrospect-url",
authFn: nil,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := newResourceServer(context.Background(), tt.args.issuer, tt.args.authorizer, tt.args.options...)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
if tt.wantFields == nil {
return
}
assert.Equal(t, tt.wantFields.issuer, got.issuer)
assert.Equal(t, tt.wantFields.tokenURL, got.tokenURL)
assert.Equal(t, tt.wantFields.introspectURL, got.introspectURL)
})
}
}
func TestIntrospect(t *testing.T) {
type args struct {
ctx context.Context
rp ResourceServer
token string
}
rp, err := newResourceServer(
context.Background(),
"https://accounts.spotify.com",
nil,
)
require.NoError(t, err)
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "missing-introspect-url",
args: args{
ctx: context.Background(),
rp: rp,
token: "my-token",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Introspect[*oidc.IntrospectionResponse](tt.args.ctx, tt.args.rp, tt.args.token)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
})
}
}

View file

@ -1,52 +1,38 @@
package tokenexchange package tokenexchange
import ( import (
"context"
"errors" "errors"
"net/http" "net/http"
"time"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client" "github.com/zitadel/oidc/v2/pkg/client"
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/go-jose/go-jose/v4"
) )
type TokenExchanger interface { type TokenExchanger interface {
TokenEndpoint() string TokenEndpoint() string
HttpClient() *http.Client HttpClient() *http.Client
AuthFn() (any, error) AuthFn() (interface{}, error)
} }
type OAuthTokenExchange struct { type OAuthTokenExchange struct {
httpClient *http.Client httpClient *http.Client
tokenEndpoint string tokenEndpoint string
authFn func() (any, error) authFn func() (interface{}, error)
} }
func NewTokenExchanger(ctx context.Context, issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { func NewTokenExchanger(issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
return newOAuthTokenExchange(ctx, issuer, nil, options...) return newOAuthTokenExchange(issuer, nil, options...)
} }
func NewTokenExchangerClientCredentials(ctx context.Context, issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { func NewTokenExchangerClientCredentials(issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
authorizer := func() (any, error) { authorizer := func() (interface{}, error) {
return httphelper.AuthorizeBasic(clientID, clientSecret), nil return httphelper.AuthorizeBasic(clientID, clientSecret), nil
} }
return newOAuthTokenExchange(ctx, issuer, authorizer, options...) return newOAuthTokenExchange(issuer, authorizer, options...)
} }
func NewTokenExchangerJWTProfile(ctx context.Context, issuer, clientID string, signer jose.Signer, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) { func newOAuthTokenExchange(issuer string, authorizer func() (interface{}, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) {
authorizer := func() (any, error) {
assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer)
if err != nil {
return nil, err
}
return client.ClientAssertionFormAuthorization(assertion), nil
}
return newOAuthTokenExchange(ctx, issuer, authorizer, options...)
}
func newOAuthTokenExchange(ctx context.Context, issuer string, authorizer func() (any, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) {
te := &OAuthTokenExchange{ te := &OAuthTokenExchange{
httpClient: httphelper.DefaultHTTPClient, httpClient: httphelper.DefaultHTTPClient,
} }
@ -55,7 +41,7 @@ func newOAuthTokenExchange(ctx context.Context, issuer string, authorizer func()
} }
if te.tokenEndpoint == "" { if te.tokenEndpoint == "" {
config, err := client.Discover(ctx, issuer, te.httpClient) config, err := client.Discover(issuer, te.httpClient)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -92,7 +78,7 @@ func (te *OAuthTokenExchange) HttpClient() *http.Client {
return te.httpClient return te.httpClient
} }
func (te *OAuthTokenExchange) AuthFn() (any, error) { func (te *OAuthTokenExchange) AuthFn() (interface{}, error) {
if te.authFn != nil { if te.authFn != nil {
return te.authFn() return te.authFn()
} }
@ -103,7 +89,6 @@ func (te *OAuthTokenExchange) AuthFn() (any, error) {
// ExchangeToken sends a token exchange request (rfc 8693) to te's token endpoint. // ExchangeToken sends a token exchange request (rfc 8693) to te's token endpoint.
// SubjectToken and SubjectTokenType are required parameters. // SubjectToken and SubjectTokenType are required parameters.
func ExchangeToken( func ExchangeToken(
ctx context.Context,
te TokenExchanger, te TokenExchanger,
SubjectToken string, SubjectToken string,
SubjectTokenType oidc.TokenType, SubjectTokenType oidc.TokenType,
@ -114,9 +99,6 @@ func ExchangeToken(
Scopes []string, Scopes []string,
RequestedTokenType oidc.TokenType, RequestedTokenType oidc.TokenType,
) (*oidc.TokenExchangeResponse, error) { ) (*oidc.TokenExchangeResponse, error) {
ctx, span := client.Tracer.Start(ctx, "ExchangeToken")
defer span.End()
if SubjectToken == "" { if SubjectToken == "" {
return nil, errors.New("empty subject_token") return nil, errors.New("empty subject_token")
} }
@ -141,5 +123,5 @@ func ExchangeToken(
RequestedTokenType: RequestedTokenType, RequestedTokenType: RequestedTokenType,
} }
return client.CallTokenExchangeEndpoint(ctx, request, authFn, te) return client.CallTokenExchangeEndpoint(request, authFn, te)
} }

View file

@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"hash" "hash"
jose "github.com/go-jose/go-jose/v4" "gopkg.in/square/go-jose.v2"
) )
var ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm") var ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm")
@ -21,14 +21,6 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) {
return sha512.New384(), nil return sha512.New384(), nil
case jose.RS512, jose.ES512, jose.PS512: case jose.RS512, jose.ES512, jose.PS512:
return sha512.New(), nil return sha512.New(), nil
// There is no published spec for this yet, but we have confirmation it will get published.
// There is consensus here: https://bitbucket.org/openid/connect/issues/1125/_hash-algorithm-for-eddsa-id-tokens
// Currently Go and go-jose only supports the ed25519 curve key for EdDSA, so we can safely assume sha512 here.
// It is unlikely ed448 will ever be supported: https://github.com/golang/go/issues/29390
case jose.EdDSA:
return sha512.New(), nil
default: default:
return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm) return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm)
} }

View file

@ -1,45 +1,17 @@
package crypto package crypto
import ( import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors"
"github.com/go-jose/go-jose/v4"
) )
var ( func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
ErrPEMDecode = errors.New("PEM decode failed") block, _ := pem.Decode(priv)
ErrUnsupportedFormat = errors.New("key is neither in PKCS#1 nor PKCS#8 format") b := block.Bytes
ErrUnsupportedPrivateKey = errors.New("unsupported key type, must be RSA, ECDSA or ED25519 private key") key, err := x509.ParsePKCS1PrivateKey(b)
)
func BytesToPrivateKey(b []byte) (crypto.PublicKey, jose.SignatureAlgorithm, error) {
block, _ := pem.Decode(b)
if block == nil {
return nil, "", ErrPEMDecode
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err == nil {
return privateKey, jose.RS256, nil
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil { if err != nil {
return nil, "", ErrUnsupportedFormat return nil, err
}
switch privateKey := key.(type) {
case *rsa.PrivateKey:
return privateKey, jose.RS256, nil
case ed25519.PrivateKey:
return privateKey, jose.EdDSA, nil
case *ecdsa.PrivateKey:
return privateKey, jose.ES256, nil
default:
return nil, "", ErrUnsupportedPrivateKey
} }
return key, nil
} }

View file

@ -1,134 +0,0 @@
package crypto_test
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"testing"
"github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/assert"
zcrypto "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
)
func TestBytesToPrivateKey(t *testing.T) {
type args struct {
key []byte
}
type want struct {
key crypto.Signer
algorithm jose.SignatureAlgorithm
err error
}
tests := []struct {
name string
args args
want want
}{
{
name: "PEMDecodeError",
args: args{
key: []byte("The non-PEM sequence"),
},
want: want{
err: zcrypto.ErrPEMDecode,
},
},
{
name: "PKCS#1 RSA",
args: args{
key: []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
-----END RSA PRIVATE KEY-----`),
},
want: want{
key: &rsa.PrivateKey{},
algorithm: jose.RS256,
err: nil,
},
},
{
name: "PKCS#8 RSA",
args: args{
key: []byte(`-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfaDB7pK/fmP/I
7IusSK8lTCBnPZghqIbVLt2QHYAMoEF1CaF4F4rxo2vl1Mt8gwsq4T3osQFZMvnL
YHb7KNyUoJgTjLxJQADv2u4Q3U38heAzK5Tp4ry4MCnuyJIqAPK1GiruwEq4zQrx
+WzVix8otO37SuW9tzklqlNGMiAYBL0TBKHvS5XMbjP1idBMB8erMz29w/TVQnEB
Kj0vCdZjrbVPKygptt5kcSrL5f4xCZwU+ufz7cp0GLwpRMJ+shG9YJJFBxb0itPF
sy51vAyEtdBC7jgAU96ZVeQ06nryDq1D2EpoVMElqNyL46Jo3lnKbGquGKzXzQYU
BN32/scDAgMBAAECggEBAJE/mo3PLgILo2YtQ8ekIxNVHmF0Gl7w9IrjvTdH6hmX
HI3MTLjkmtI7GmG9V/0IWvCjdInGX3grnrjWGRQZ04QKIQgPQLFuBGyJjEsJm7nx
MqztlS7YTyV1nX/aenSTkJO8WEpcJLnm+4YoxCaAMdAhrIdBY71OamALpv1bRysa
FaiCGcemT2yqZn0GqIS8O26Tz5zIqrTN2G1eSmgh7DG+7FoddMz35cute8R10xUG
hF5YU+6fcXiRQ/Kh7nlxelPGqdZFPMk7LpVHzkQKwdJ+N0P23lPDIfNsvpG1n0OP
3g5km7gHSrSU2yZ3eFl6DB9x1IFNS9BaQQuSxYJtKwECgYEA1C8jjzpXZDLvlYsV
2jlMzkrbsIrX2dzblVrNsPs2jRbjYU8mg2DUDO6lOhtxHfqZG6sO+gmWi/zvoy9l
yolGbXe1Jqx66p9fznIcecSwar8+ACa356Wk74Nt1PlBOfCMqaJnYLOLaFJa29Vy
u5ClZVzKd5AVXl7yFVd4XfLv/WECgYEAwFMMtFoasdF92c0d31rZ1uoPOtFz6xq6
uQggdm5zzkhnfwUAGqppS/u1CHcJ7T/74++jLbFTsaohGr4jEzWSGvJpomEUChy3
r25YofMclUhJ5pCEStsLtqiCR1Am6LlI8HMdBEP1QDgEC5q8bQW4+UHuew1E1zxz
osZOhe09WuMCgYEA0G9aFCnwjUqIFjQiDFP7gi8BLqTFs4uE3Wvs4W11whV42i+B
ms90nxuTjchFT3jMDOT1+mOO0wdudLRr3xEI8SIF/u6ydGaJG+j21huEXehtxIJE
aDdNFcfbDbqo+3y1ATK7MMBPMvSrsoY0hdJq127WqasNgr3sO1DIuima3SECgYEA
nkM5TyhekzlbIOHD1UsDu/D7+2DkzPE/+oePfyXBMl0unb3VqhvVbmuBO6gJiSx/
8b//PdiQkMD5YPJaFrKcuoQFHVRZk0CyfzCEyzAts0K7XXpLAvZiGztriZeRjSz7
srJnjF0H8oKmAY6hw+1Tm/n/b08p+RyL48TgVSE2vhUCgYA3BWpkD4PlCcn/FZsq
OrLFyFXI6jIaxskFtsRW1IxxIlAdZmxfB26P/2gx6VjLdxJI/RRPkJyEN2dP7CbR
BDjb565dy1O9D6+UrY70Iuwjz+OcALRBBGTaiF2pLn6IhSzNI2sy/tXX8q8dBlg9
OFCrqT/emes3KytTPfa5NZtYeQ==
-----END PRIVATE KEY-----`),
},
want: want{
key: &rsa.PrivateKey{},
algorithm: jose.RS256,
err: nil,
},
},
{
name: "PKCS#8 ECDSA",
args: args{
key: []byte(`-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwwOZSU4GlP7ps/Wp
V6o0qRwxultdfYo/uUuj48QZjSuhRANCAATMiI2Han+ABKmrk5CNlxRAGC61w4d3
G4TAeuBpyzqJ7x/6NjCxoQzJzZHtNjIfjVATI59XFZWF59GhtSZbShAr
-----END PRIVATE KEY-----`),
},
want: want{
key: &ecdsa.PrivateKey{},
algorithm: jose.ES256,
err: nil,
},
},
{
name: "PKCS#8 ED25519",
args: args{
key: []byte(`-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIHu6ZtDsjjauMasBxnS9Fg87UJwKfcT/oiq6S0ktbky8
-----END PRIVATE KEY-----`),
},
want: want{
key: ed25519.PrivateKey{},
algorithm: jose.EdDSA,
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key, algorithm, err := zcrypto.BytesToPrivateKey(tt.args.key)
assert.IsType(t, tt.want.key, key)
assert.Equal(t, tt.want.algorithm, algorithm)
assert.ErrorIs(t, tt.want.err, err)
})
}
}

View file

@ -4,10 +4,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
jose "github.com/go-jose/go-jose/v4" "gopkg.in/square/go-jose.v2"
) )
func Sign(object any, signer jose.Signer) (string, error) { func Sign(object interface{}, signer jose.Signer) (string, error) {
payload, err := json.Marshal(object) payload, err := json.Marshal(object)
if err != nil { if err != nil {
return "", err return "", err

View file

@ -10,8 +10,6 @@ import (
"net/url" "net/url"
"strings" "strings"
"time" "time"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
var DefaultHTTPClient = &http.Client{ var DefaultHTTPClient = &http.Client{
@ -19,11 +17,11 @@ var DefaultHTTPClient = &http.Client{
} }
type Decoder interface { type Decoder interface {
Decode(dst any, src map[string][]string) error Decode(dst interface{}, src map[string][]string) error
} }
type Encoder interface { type Encoder interface {
Encode(src any, dst map[string][]string) error Encode(src interface{}, dst map[string][]string) error
} }
type FormAuthorization func(url.Values) type FormAuthorization func(url.Values)
@ -35,7 +33,7 @@ func AuthorizeBasic(user, password string) RequestAuthorization {
} }
} }
func FormRequest(ctx context.Context, endpoint string, request any, encoder Encoder, authFn any) (*http.Request, error) { func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn interface{}) (*http.Request, error) {
form := url.Values{} form := url.Values{}
if err := encoder.Encode(request, form); err != nil { if err := encoder.Encode(request, form); err != nil {
return nil, err return nil, err
@ -44,7 +42,7 @@ func FormRequest(ctx context.Context, endpoint string, request any, encoder Enco
fn(form) fn(form)
} }
body := strings.NewReader(form.Encode()) body := strings.NewReader(form.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) req, err := http.NewRequest("POST", endpoint, body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -55,7 +53,7 @@ func FormRequest(ctx context.Context, endpoint string, request any, encoder Enco
return req, nil return req, nil
} }
func HttpRequest(client *http.Client, req *http.Request, response any) error { func HttpRequest(client *http.Client, req *http.Request, response interface{}) error {
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
@ -68,12 +66,7 @@ func HttpRequest(client *http.Client, req *http.Request, response any) error {
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
var oidcErr oidc.Error return fmt.Errorf("http status not ok: %s %s", resp.Status, body)
err = json.Unmarshal(body, &oidcErr)
if err != nil || oidcErr.ErrorType == "" {
return fmt.Errorf("http status not ok: %s %s", resp.Status, body)
}
return &oidcErr
} }
err = json.Unmarshal(body, response) err = json.Unmarshal(body, response)
@ -83,7 +76,7 @@ func HttpRequest(client *http.Client, req *http.Request, response any) error {
return nil return nil
} }
func URLEncodeParams(resp any, encoder Encoder) (url.Values, error) { func URLEncodeParams(resp interface{}, encoder Encoder) (url.Values, error) {
values := make(map[string][]string) values := make(map[string][]string)
err := encoder.Encode(resp, values) err := encoder.Encode(resp, values)
if err != nil { if err != nil {

View file

@ -8,11 +8,11 @@ import (
"reflect" "reflect"
) )
func MarshalJSON(w http.ResponseWriter, i any) { func MarshalJSON(w http.ResponseWriter, i interface{}) {
MarshalJSONWithStatus(w, i, http.StatusOK) MarshalJSONWithStatus(w, i, http.StatusOK)
} }
func MarshalJSONWithStatus(w http.ResponseWriter, i any, status int) { func MarshalJSONWithStatus(w http.ResponseWriter, i interface{}, status int) {
w.Header().Set("content-type", "application/json") w.Header().Set("content-type", "application/json")
w.WriteHeader(status) w.WriteHeader(status)
if i == nil || (reflect.ValueOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) { if i == nil || (reflect.ValueOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) {

View file

@ -94,7 +94,7 @@ func TestConcatenateJSON(t *testing.T) {
func TestMarshalJSONWithStatus(t *testing.T) { func TestMarshalJSONWithStatus(t *testing.T) {
type args struct { type args struct {
i any i interface{}
status int status int
} }
type res struct { type res struct {

View file

@ -1,9 +1,5 @@
package oidc package oidc
import (
"log/slog"
)
const ( const (
// ScopeOpenID defines the scope `openid` // ScopeOpenID defines the scope `openid`
// OpenID Connect requests MUST contain the `openid` scope value // OpenID Connect requests MUST contain the `openid` scope value
@ -48,7 +44,6 @@ const (
ResponseModeQuery ResponseMode = "query" ResponseModeQuery ResponseMode = "query"
ResponseModeFragment ResponseMode = "fragment" ResponseModeFragment ResponseMode = "fragment"
ResponseModeFormPost ResponseMode = "form_post"
// PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages. // PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages.
// An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed // An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed
@ -65,7 +60,7 @@ const (
) )
// AuthRequest according to: // AuthRequest according to:
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest //https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
type AuthRequest struct { type AuthRequest struct {
Scopes SpaceDelimitedArray `json:"scope" schema:"scope"` Scopes SpaceDelimitedArray `json:"scope" schema:"scope"`
ResponseType ResponseType `json:"response_type" schema:"response_type"` ResponseType ResponseType `json:"response_type" schema:"response_type"`
@ -82,7 +77,7 @@ type AuthRequest struct {
UILocales Locales `json:"ui_locales" schema:"ui_locales"` UILocales Locales `json:"ui_locales" schema:"ui_locales"`
IDTokenHint string `json:"id_token_hint" schema:"id_token_hint"` IDTokenHint string `json:"id_token_hint" schema:"id_token_hint"`
LoginHint string `json:"login_hint" schema:"login_hint"` LoginHint string `json:"login_hint" schema:"login_hint"`
ACRValues SpaceDelimitedArray `json:"acr_values" schema:"acr_values"` ACRValues []string `json:"acr_values" schema:"acr_values"`
CodeChallenge string `json:"code_challenge" schema:"code_challenge"` CodeChallenge string `json:"code_challenge" schema:"code_challenge"`
CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method" schema:"code_challenge_method"` CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method" schema:"code_challenge_method"`
@ -91,15 +86,6 @@ type AuthRequest struct {
RequestParam string `schema:"request"` RequestParam string `schema:"request"`
} }
func (a *AuthRequest) LogValue() slog.Value {
return slog.GroupValue(
slog.Any("scopes", a.Scopes),
slog.String("response_type", string(a.ResponseType)),
slog.String("client_id", a.ClientID),
slog.String("redirect_uri", a.RedirectURI),
)
}
// GetRedirectURI returns the redirect_uri value for the ErrAuthRequest interface // GetRedirectURI returns the redirect_uri value for the ErrAuthRequest interface
func (a *AuthRequest) GetRedirectURI() string { func (a *AuthRequest) GetRedirectURI() string {
return a.RedirectURI return a.RedirectURI
@ -114,8 +100,3 @@ func (a *AuthRequest) GetResponseType() ResponseType {
func (a *AuthRequest) GetState() string { func (a *AuthRequest) GetState() string {
return a.State return a.State
} }
// GetResponseMode returns the optional ResponseMode
func (a *AuthRequest) GetResponseMode() ResponseMode {
return a.ResponseMode
}

View file

@ -1,27 +0,0 @@
//go:build go1.20
package oidc
import (
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAuthRequest_LogValue(t *testing.T) {
a := &AuthRequest{
Scopes: SpaceDelimitedArray{"a", "b"},
ResponseType: "respType",
ClientID: "123",
RedirectURI: "http://example.com/callback",
}
want := slog.GroupValue(
slog.Any("scopes", SpaceDelimitedArray{"a", "b"}),
slog.String("response_type", "respType"),
slog.String("client_id", "123"),
slog.String("redirect_uri", "http://example.com/callback"),
)
got := a.LogValue()
assert.Equal(t, want, got)
}

View file

@ -3,7 +3,7 @@ package oidc
import ( import (
"crypto/sha256" "crypto/sha256"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto" "github.com/zitadel/oidc/v2/pkg/crypto"
) )
const ( const (

View file

@ -1,7 +1,5 @@
package oidc package oidc
import "encoding/json"
// DeviceAuthorizationRequest implements // DeviceAuthorizationRequest implements
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1, // https://www.rfc-editor.org/rfc/rfc8628#section-3.1,
// 3.1 Device Authorization Request. // 3.1 Device Authorization Request.
@ -22,26 +20,6 @@ type DeviceAuthorizationResponse struct {
Interval int `json:"interval,omitempty"` Interval int `json:"interval,omitempty"`
} }
func (resp *DeviceAuthorizationResponse) UnmarshalJSON(data []byte) error {
type Alias DeviceAuthorizationResponse
aux := &struct {
// workaround misspelling of verification_uri
// https://stackoverflow.com/q/76696956/5690223
// https://developers.google.com/identity/protocols/oauth2/limited-input-device?hl=fr#success-response
VerificationURL string `json:"verification_url"`
*Alias
}{
Alias: (*Alias)(resp),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if resp.VerificationURI == "" {
resp.VerificationURI = aux.VerificationURL
}
return nil
}
// DeviceAccessTokenRequest implements // DeviceAccessTokenRequest implements
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4, // https://www.rfc-editor.org/rfc/rfc8628#section-3.4,
// Device Access Token Request. // Device Access Token Request.

View file

@ -1,30 +0,0 @@
package oidc
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDeviceAuthorizationResponse_UnmarshalJSON(t *testing.T) {
jsonStr := `{
"device_code": "deviceCode",
"user_code": "userCode",
"verification_url": "http://example.com/verify",
"expires_in": 3600,
"interval": 5
}`
expected := &DeviceAuthorizationResponse{
DeviceCode: "deviceCode",
UserCode: "userCode",
VerificationURI: "http://example.com/verify",
ExpiresIn: 3600,
Interval: 5,
}
var resp DeviceAuthorizationResponse
err := resp.UnmarshalJSON([]byte(jsonStr))
assert.NoError(t, err)
assert.Equal(t, expected, &resp)
}

View file

@ -1,5 +1,9 @@
package oidc package oidc
import (
"golang.org/x/text/language"
)
const ( const (
DiscoveryEndpoint = "/.well-known/openid-configuration" DiscoveryEndpoint = "/.well-known/openid-configuration"
) )
@ -126,10 +130,10 @@ type DiscoveryConfiguration struct {
ServiceDocumentation string `json:"service_documentation,omitempty"` ServiceDocumentation string `json:"service_documentation,omitempty"`
// ClaimsLocalesSupported contains a list of BCP47 language tag values that the OP supports for values of Claims returned. // ClaimsLocalesSupported contains a list of BCP47 language tag values that the OP supports for values of Claims returned.
ClaimsLocalesSupported Locales `json:"claims_locales_supported,omitempty"` ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"`
// UILocalesSupported contains a list of BCP47 language tag values that the OP supports for the user interface. // UILocalesSupported contains a list of BCP47 language tag values that the OP supports for the user interface.
UILocalesSupported Locales `json:"ui_locales_supported,omitempty"` UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"`
// RequestParameterSupported specifies whether the OP supports use of the `request` parameter. If omitted, the default value is false. // RequestParameterSupported specifies whether the OP supports use of the `request` parameter. If omitted, the default value is false.
RequestParameterSupported bool `json:"request_parameter_supported,omitempty"` RequestParameterSupported bool `json:"request_parameter_supported,omitempty"`
@ -145,14 +149,6 @@ type DiscoveryConfiguration struct {
// OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service. // OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service.
OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"` OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
// BackChannelLogoutSupported specifies whether the OP supports back-channel logout (https://openid.net/specs/openid-connect-backchannel-1_0.html),
// with true indicating support. If omitted, the default value is false.
BackChannelLogoutSupported bool `json:"backchannel_logout_supported,omitempty"`
// BackChannelLogoutSessionSupported specifies whether the OP can pass a sid (session ID) Claim in the Logout Token to identify the RP session with the OP.
// If supported, the sid Claim is also included in ID Tokens issued by the OP. If omitted, the default value is false.
BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported,omitempty"`
} }
type AuthMethod string type AuthMethod string

View file

@ -1,10 +1,8 @@
package oidc package oidc
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog"
) )
type errorType string type errorType string
@ -28,11 +26,6 @@ const (
SlowDown errorType = "slow_down" SlowDown errorType = "slow_down"
AccessDenied errorType = "access_denied" AccessDenied errorType = "access_denied"
ExpiredToken errorType = "expired_token" ExpiredToken errorType = "expired_token"
// InvalidTarget error is returned by Token Exchange if
// the requested target or audience is invalid.
// [RFC 8693, Section 2.2.2: Error Response](https://www.rfc-editor.org/rfc/rfc8693#section-2.2.2)
InvalidTarget errorType = "invalid_target"
) )
var ( var (
@ -118,14 +111,6 @@ var (
Description: "The \"device_code\" has expired.", Description: "The \"device_code\" has expired.",
} }
} }
// Token exchange error
ErrInvalidTarget = func() *Error {
return &Error{
ErrorType: InvalidTarget,
Description: "The requested audience or target is invalid.",
}
}
) )
type Error struct { type Error struct {
@ -133,28 +118,7 @@ type Error struct {
ErrorType errorType `json:"error" schema:"error"` ErrorType errorType `json:"error" schema:"error"`
Description string `json:"error_description,omitempty" schema:"error_description,omitempty"` Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
State string `json:"state,omitempty" schema:"state,omitempty"` State string `json:"state,omitempty" schema:"state,omitempty"`
SessionState string `json:"session_state,omitempty" schema:"session_state,omitempty"`
redirectDisabled bool `schema:"-"` redirectDisabled bool `schema:"-"`
returnParent bool `schema:"-"`
}
func (e *Error) MarshalJSON() ([]byte, error) {
m := struct {
Error errorType `json:"error"`
ErrorDescription string `json:"error_description,omitempty"`
State string `json:"state,omitempty"`
SessionState string `json:"session_state,omitempty"`
Parent string `json:"parent,omitempty"`
}{
Error: e.ErrorType,
ErrorDescription: e.Description,
State: e.State,
SessionState: e.SessionState,
}
if e.returnParent {
m.Parent = e.Parent.Error()
}
return json.Marshal(m)
} }
func (e *Error) Error() string { func (e *Error) Error() string {
@ -179,8 +143,7 @@ func (e *Error) Is(target error) bool {
} }
return e.ErrorType == t.ErrorType && return e.ErrorType == t.ErrorType &&
(e.Description == t.Description || t.Description == "") && (e.Description == t.Description || t.Description == "") &&
(e.State == t.State || t.State == "") && (e.State == t.State || t.State == "")
(e.SessionState == t.SessionState || t.SessionState == "")
} }
func (e *Error) WithParent(err error) *Error { func (e *Error) WithParent(err error) *Error {
@ -188,19 +151,7 @@ func (e *Error) WithParent(err error) *Error {
return e return e
} }
// WithReturnParentToClient allows returning the set parent error to the HTTP client. func (e *Error) WithDescription(desc string, args ...interface{}) *Error {
// Currently it only supports setting the parent inside JSON responses, not redirect URLs.
// As Go errors don't unmarshal well, only the marshaller is implemented for the moment.
//
// Warning: parent errors may contain sensitive data or unwanted details about the server status.
// Also, the `parent` field is not a standard error field and might confuse certain clients
// that require fully compliant responses.
func (e *Error) WithReturnParentToClient(b bool) *Error {
e.returnParent = b
return e
}
func (e *Error) WithDescription(desc string, args ...any) *Error {
e.Description = fmt.Sprintf(desc, args...) e.Description = fmt.Sprintf(desc, args...)
return e return e
} }
@ -220,37 +171,3 @@ func DefaultToServerError(err error, description string) *Error {
} }
return oauth return oauth
} }
func (e *Error) LogLevel() slog.Level {
level := slog.LevelWarn
if e.ErrorType == ServerError {
level = slog.LevelError
}
if e.ErrorType == AuthorizationPending {
level = slog.LevelInfo
}
return level
}
func (e *Error) LogValue() slog.Value {
attrs := make([]slog.Attr, 0, 5)
if e.Parent != nil {
attrs = append(attrs, slog.Any("parent", e.Parent))
}
if e.Description != "" {
attrs = append(attrs, slog.String("description", e.Description))
}
if e.ErrorType != "" {
attrs = append(attrs, slog.String("type", string(e.ErrorType)))
}
if e.State != "" {
attrs = append(attrs, slog.String("state", e.State))
}
if e.SessionState != "" {
attrs = append(attrs, slog.String("session_state", e.SessionState))
}
if e.redirectDisabled {
attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled))
}
return slog.GroupValue(attrs...)
}

View file

@ -1,192 +0,0 @@
package oidc
import (
"encoding/json"
"errors"
"io"
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDefaultToServerError(t *testing.T) {
type args struct {
err error
description string
}
tests := []struct {
name string
args args
want *Error
}{
{
name: "default",
args: args{
err: io.ErrClosedPipe,
description: "oops",
},
want: &Error{
ErrorType: ServerError,
Description: "oops",
Parent: io.ErrClosedPipe,
},
},
{
name: "our Error",
args: args{
err: ErrAccessDenied(),
description: "oops",
},
want: &Error{
ErrorType: AccessDenied,
Description: "The authorization request was denied.",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := DefaultToServerError(tt.args.err, tt.args.description)
assert.ErrorIs(t, got, tt.want)
})
}
}
func TestError_LogLevel(t *testing.T) {
tests := []struct {
name string
err *Error
want slog.Level
}{
{
name: "server error",
err: ErrServerError(),
want: slog.LevelError,
},
{
name: "authorization pending",
err: ErrAuthorizationPending(),
want: slog.LevelInfo,
},
{
name: "some other error",
err: ErrAccessDenied(),
want: slog.LevelWarn,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.err.LogLevel()
assert.Equal(t, tt.want, got)
})
}
}
func TestError_LogValue(t *testing.T) {
type fields struct {
Parent error
ErrorType errorType
Description string
State string
redirectDisabled bool
}
tests := []struct {
name string
fields fields
want slog.Value
}{
{
name: "parent",
fields: fields{
Parent: io.EOF,
},
want: slog.GroupValue(slog.Any("parent", io.EOF)),
},
{
name: "description",
fields: fields{
Description: "oops",
},
want: slog.GroupValue(slog.String("description", "oops")),
},
{
name: "errorType",
fields: fields{
ErrorType: ExpiredToken,
},
want: slog.GroupValue(slog.String("type", string(ExpiredToken))),
},
{
name: "state",
fields: fields{
State: "123",
},
want: slog.GroupValue(slog.String("state", "123")),
},
{
name: "all fields",
fields: fields{
Parent: io.EOF,
Description: "oops",
ErrorType: ExpiredToken,
State: "123",
},
want: slog.GroupValue(
slog.Any("parent", io.EOF),
slog.String("description", "oops"),
slog.String("type", string(ExpiredToken)),
slog.String("state", "123"),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &Error{
Parent: tt.fields.Parent,
ErrorType: tt.fields.ErrorType,
Description: tt.fields.Description,
State: tt.fields.State,
redirectDisabled: tt.fields.redirectDisabled,
}
got := e.LogValue()
assert.Equal(t, tt.want, got)
})
}
}
func TestError_MarshalJSON(t *testing.T) {
tests := []struct {
name string
e *Error
want string
}{
{
name: "simple error",
e: ErrAccessDenied(),
want: `{"error":"access_denied","error_description":"The authorization request was denied."}`,
},
{
name: "with description",
e: ErrAccessDenied().WithDescription("oops"),
want: `{"error":"access_denied","error_description":"oops"}`,
},
{
name: "with parent",
e: ErrServerError().WithParent(errors.New("oops")),
want: `{"error":"server_error"}`,
},
{
name: "with return parent",
e: ErrServerError().WithParent(errors.New("oops")).WithReturnParentToClient(true),
want: `{"error":"server_error","parent":"oops"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := json.Marshal(tt.e)
require.NoError(t, err)
assert.JSONEq(t, tt.want, string(got))
})
}
}

View file

@ -1,6 +1,12 @@
package oidc package oidc
import "github.com/muhlemmer/gu" import (
"encoding/json"
"fmt"
"time"
"golang.org/x/text/language"
)
type IntrospectionRequest struct { type IntrospectionRequest struct {
Token string `schema:"token"` Token string `schema:"token"`
@ -11,69 +17,364 @@ type ClientAssertionParams struct {
ClientAssertionType string `schema:"client_assertion_type"` ClientAssertionType string `schema:"client_assertion_type"`
} }
// IntrospectionResponse implements RFC 7662, section 2.2 and type IntrospectionResponse interface {
// OpenID Connect Core 1.0, section 5.1 (UserInfo). UserInfoSetter
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2. IsActive() bool
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims. SetActive(bool)
type IntrospectionResponse struct { SetScopes(scopes []string)
Active bool `json:"active"` SetClientID(id string)
Scope SpaceDelimitedArray `json:"scope,omitempty"` SetTokenType(tokenType string)
ClientID string `json:"client_id,omitempty"` SetExpiration(exp time.Time)
TokenType string `json:"token_type,omitempty"` SetIssuedAt(iat time.Time)
Expiration Time `json:"exp,omitempty"` SetNotBefore(nbf time.Time)
IssuedAt Time `json:"iat,omitempty"` SetAudience(audience []string)
AuthTime Time `json:"auth_time,omitempty"` SetIssuer(issuer string)
NotBefore Time `json:"nbf,omitempty"` SetJWTID(id string)
Subject string `json:"sub,omitempty"` GetScope() []string
Audience Audience `json:"aud,omitempty"` GetClientID() string
AuthenticationMethodsReferences []string `json:"amr,omitempty"` GetTokenType() string
Issuer string `json:"iss,omitempty"` GetExpiration() time.Time
JWTID string `json:"jti,omitempty"` GetIssuedAt() time.Time
Username string `json:"username,omitempty"` GetNotBefore() time.Time
Actor *ActorClaims `json:"act,omitempty"` GetSubject() string
UserInfoProfile GetAudience() []string
UserInfoEmail GetIssuer() string
UserInfoPhone GetJWTID() string
Address *UserInfoAddress `json:"address,omitempty"`
Claims map[string]any `json:"-"`
} }
// SetUserInfo copies all relevant fields from UserInfo func NewIntrospectionResponse() IntrospectionResponse {
// into the IntroSpectionResponse. return &introspectionResponse{}
func (i *IntrospectionResponse) SetUserInfo(u *UserInfo) {
i.Subject = u.Subject
i.Username = u.PreferredUsername
i.Address = gu.PtrCopy(u.Address)
i.UserInfoProfile = u.UserInfoProfile
i.UserInfoEmail = u.UserInfoEmail
i.UserInfoPhone = u.UserInfoPhone
if i.Claims == nil {
i.Claims = gu.MapCopy(u.Claims)
} else {
gu.MapMerge(u.Claims, i.Claims)
}
} }
// GetAddress is a safe getter that takes type introspectionResponse struct {
// care of a possible nil value. Active bool `json:"active"`
func (i *IntrospectionResponse) GetAddress() *UserInfoAddress { Scope SpaceDelimitedArray `json:"scope,omitempty"`
if i.Address == nil { ClientID string `json:"client_id,omitempty"`
return new(UserInfoAddress) TokenType string `json:"token_type,omitempty"`
} Expiration Time `json:"exp,omitempty"`
IssuedAt Time `json:"iat,omitempty"`
NotBefore Time `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
Audience Audience `json:"aud,omitempty"`
Issuer string `json:"iss,omitempty"`
JWTID string `json:"jti,omitempty"`
userInfoProfile
userInfoEmail
userInfoPhone
Address UserInfoAddress `json:"address,omitempty"`
claims map[string]interface{}
}
func (i *introspectionResponse) IsActive() bool {
return i.Active
}
func (i *introspectionResponse) GetSubject() string {
return i.Subject
}
func (i *introspectionResponse) GetName() string {
return i.Name
}
func (i *introspectionResponse) GetGivenName() string {
return i.GivenName
}
func (i *introspectionResponse) GetFamilyName() string {
return i.FamilyName
}
func (i *introspectionResponse) GetMiddleName() string {
return i.MiddleName
}
func (i *introspectionResponse) GetNickname() string {
return i.Nickname
}
func (i *introspectionResponse) GetProfile() string {
return i.Profile
}
func (i *introspectionResponse) GetPicture() string {
return i.Picture
}
func (i *introspectionResponse) GetWebsite() string {
return i.Website
}
func (i *introspectionResponse) GetGender() Gender {
return i.Gender
}
func (i *introspectionResponse) GetBirthdate() string {
return i.Birthdate
}
func (i *introspectionResponse) GetZoneinfo() string {
return i.Zoneinfo
}
func (i *introspectionResponse) GetLocale() language.Tag {
return i.Locale
}
func (i *introspectionResponse) GetPreferredUsername() string {
return i.PreferredUsername
}
func (i *introspectionResponse) GetEmail() string {
return i.Email
}
func (i *introspectionResponse) IsEmailVerified() bool {
return bool(i.EmailVerified)
}
func (i *introspectionResponse) GetPhoneNumber() string {
return i.PhoneNumber
}
func (i *introspectionResponse) IsPhoneNumberVerified() bool {
return i.PhoneNumberVerified
}
func (i *introspectionResponse) GetAddress() UserInfoAddress {
return i.Address return i.Address
} }
// introspectionResponseAlias prevents loops on the JSON methods func (i *introspectionResponse) GetClaim(key string) interface{} {
type introspectionResponseAlias IntrospectionResponse return i.claims[key]
}
func (i *IntrospectionResponse) MarshalJSON() ([]byte, error) { func (i *introspectionResponse) GetClaims() map[string]interface{} {
if i.Username == "" { return i.claims
i.Username = i.PreferredUsername }
func (i *introspectionResponse) GetScope() []string {
return []string(i.Scope)
}
func (i *introspectionResponse) GetClientID() string {
return i.ClientID
}
func (i *introspectionResponse) GetTokenType() string {
return i.TokenType
}
func (i *introspectionResponse) GetExpiration() time.Time {
return time.Time(i.Expiration)
}
func (i *introspectionResponse) GetIssuedAt() time.Time {
return time.Time(i.IssuedAt)
}
func (i *introspectionResponse) GetNotBefore() time.Time {
return time.Time(i.NotBefore)
}
func (i *introspectionResponse) GetAudience() []string {
return []string(i.Audience)
}
func (i *introspectionResponse) GetIssuer() string {
return i.Issuer
}
func (i *introspectionResponse) GetJWTID() string {
return i.JWTID
}
func (i *introspectionResponse) SetActive(active bool) {
i.Active = active
}
func (i *introspectionResponse) SetScopes(scope []string) {
i.Scope = scope
}
func (i *introspectionResponse) SetClientID(id string) {
i.ClientID = id
}
func (i *introspectionResponse) SetTokenType(tokenType string) {
i.TokenType = tokenType
}
func (i *introspectionResponse) SetExpiration(exp time.Time) {
i.Expiration = Time(exp)
}
func (i *introspectionResponse) SetIssuedAt(iat time.Time) {
i.IssuedAt = Time(iat)
}
func (i *introspectionResponse) SetNotBefore(nbf time.Time) {
i.NotBefore = Time(nbf)
}
func (i *introspectionResponse) SetAudience(audience []string) {
i.Audience = audience
}
func (i *introspectionResponse) SetIssuer(issuer string) {
i.Issuer = issuer
}
func (i *introspectionResponse) SetJWTID(id string) {
i.JWTID = id
}
func (i *introspectionResponse) SetSubject(sub string) {
i.Subject = sub
}
func (i *introspectionResponse) SetName(name string) {
i.Name = name
}
func (i *introspectionResponse) SetGivenName(name string) {
i.GivenName = name
}
func (i *introspectionResponse) SetFamilyName(name string) {
i.FamilyName = name
}
func (i *introspectionResponse) SetMiddleName(name string) {
i.MiddleName = name
}
func (i *introspectionResponse) SetNickname(name string) {
i.Nickname = name
}
func (i *introspectionResponse) SetUpdatedAt(date time.Time) {
i.UpdatedAt = Time(date)
}
func (i *introspectionResponse) SetProfile(profile string) {
i.Profile = profile
}
func (i *introspectionResponse) SetPicture(picture string) {
i.Picture = picture
}
func (i *introspectionResponse) SetWebsite(website string) {
i.Website = website
}
func (i *introspectionResponse) SetGender(gender Gender) {
i.Gender = gender
}
func (i *introspectionResponse) SetBirthdate(birthdate string) {
i.Birthdate = birthdate
}
func (i *introspectionResponse) SetZoneinfo(zoneInfo string) {
i.Zoneinfo = zoneInfo
}
func (i *introspectionResponse) SetLocale(locale language.Tag) {
i.Locale = locale
}
func (i *introspectionResponse) SetPreferredUsername(name string) {
i.PreferredUsername = name
}
func (i *introspectionResponse) SetEmail(email string, verified bool) {
i.Email = email
i.EmailVerified = boolString(verified)
}
func (i *introspectionResponse) SetPhone(phone string, verified bool) {
i.PhoneNumber = phone
i.PhoneNumberVerified = verified
}
func (i *introspectionResponse) SetAddress(address UserInfoAddress) {
i.Address = address
}
func (i *introspectionResponse) AppendClaims(key string, value interface{}) {
if i.claims == nil {
i.claims = make(map[string]interface{})
} }
return mergeAndMarshalClaims((*introspectionResponseAlias)(i), i.Claims) i.claims[key] = value
} }
func (i *IntrospectionResponse) UnmarshalJSON(data []byte) error { func (i *introspectionResponse) MarshalJSON() ([]byte, error) {
return unmarshalJSONMulti(data, (*introspectionResponseAlias)(i), &i.Claims) type Alias introspectionResponse
a := &struct {
*Alias
Expiration int64 `json:"exp,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Locale interface{} `json:"locale,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
Username string `json:"username,omitempty"`
}{
Alias: (*Alias)(i),
}
if !i.Locale.IsRoot() {
a.Locale = i.Locale
}
if !time.Time(i.UpdatedAt).IsZero() {
a.UpdatedAt = time.Time(i.UpdatedAt).Unix()
}
if !time.Time(i.Expiration).IsZero() {
a.Expiration = time.Time(i.Expiration).Unix()
}
if !time.Time(i.IssuedAt).IsZero() {
a.IssuedAt = time.Time(i.IssuedAt).Unix()
}
if !time.Time(i.NotBefore).IsZero() {
a.NotBefore = time.Time(i.NotBefore).Unix()
}
a.Username = i.PreferredUsername
b, err := json.Marshal(a)
if err != nil {
return nil, err
}
if len(i.claims) == 0 {
return b, nil
}
err = json.Unmarshal(b, &i.claims)
if err != nil {
return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims)
}
return json.Marshal(i.claims)
}
func (i *introspectionResponse) UnmarshalJSON(data []byte) error {
type Alias introspectionResponse
a := &struct {
*Alias
UpdatedAt int64 `json:"update_at,omitempty"`
}{
Alias: (*Alias)(i),
}
if err := json.Unmarshal(data, &a); err != nil {
return err
}
i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC())
if err := json.Unmarshal(data, &i.claims); err != nil {
return err
}
return nil
} }

View file

@ -1,79 +0,0 @@
package oidc
import (
"encoding/json"
"testing"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIntrospectionResponse_SetUserInfo(t *testing.T) {
tests := []struct {
name string
start *IntrospectionResponse
want *IntrospectionResponse
}{
{
name: "nil claims",
start: &IntrospectionResponse{},
want: &IntrospectionResponse{
Subject: userInfoData.Subject,
Username: userInfoData.PreferredUsername,
Address: userInfoData.Address,
UserInfoProfile: userInfoData.UserInfoProfile,
UserInfoEmail: userInfoData.UserInfoEmail,
UserInfoPhone: userInfoData.UserInfoPhone,
Claims: gu.MapCopy(userInfoData.Claims),
},
},
{
name: "merge claims",
start: &IntrospectionResponse{
Claims: map[string]any{
"hello": "world",
},
},
want: &IntrospectionResponse{
Subject: userInfoData.Subject,
Username: userInfoData.PreferredUsername,
Address: userInfoData.Address,
UserInfoProfile: userInfoData.UserInfoProfile,
UserInfoEmail: userInfoData.UserInfoEmail,
UserInfoPhone: userInfoData.UserInfoPhone,
Claims: map[string]any{
"foo": "bar",
"hello": "world",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.start.SetUserInfo(userInfoData)
assert.Equal(t, tt.want, tt.start)
})
}
}
func TestIntrospectionResponse_GetAddress(t *testing.T) {
// nil address
i := new(IntrospectionResponse)
assert.Equal(t, &UserInfoAddress{}, i.GetAddress())
i.Address = &UserInfoAddress{PostalCode: "1234"}
assert.Equal(t, i.Address, i.GetAddress())
}
func TestIntrospectionResponse_MarshalJSON(t *testing.T) {
got, err := json.Marshal(&IntrospectionResponse{
UserInfoProfile: UserInfoProfile{
PreferredUsername: "muhlemmer",
},
})
require.NoError(t, err)
assert.Equal(t, string(got), `{"active":false,"username":"muhlemmer","preferred_username":"muhlemmer"}`)
}

View file

@ -6,9 +6,8 @@ import (
"crypto/ed25519" "crypto/ed25519"
"crypto/rsa" "crypto/rsa"
"errors" "errors"
"strings"
jose "github.com/go-jose/go-jose/v4" "gopkg.in/square/go-jose.v2"
) )
const ( const (
@ -47,8 +46,8 @@ func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) {
// //
// will return false none or multiple match // will return false none or multiple match
// //
// deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool //deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool
// moved implementation already to FindMatchingKey //moved implementation already to FindMatchingKey
func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) { func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) {
key, err := FindMatchingKey(keyID, use, expectedAlg, keys...) key, err := FindMatchingKey(keyID, use, expectedAlg, keys...)
return key, err == nil return key, err == nil
@ -92,18 +91,18 @@ func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (k
return key, ErrKeyNone return key, ErrKeyNone
} }
func algToKeyType(key any, alg string) bool { func algToKeyType(key interface{}, alg string) bool {
if strings.HasPrefix(alg, "RS") || strings.HasPrefix(alg, "PS") { switch alg[0] {
case 'R', 'P':
_, ok := key.(*rsa.PublicKey) _, ok := key.(*rsa.PublicKey)
return ok return ok
} case 'E':
if strings.HasPrefix(alg, "ES") {
_, ok := key.(*ecdsa.PublicKey) _, ok := key.(*ecdsa.PublicKey)
return ok return ok
} case 'O':
if alg == string(jose.EdDSA) { _, ok := key.(*ed25519.PublicKey)
_, ok := key.(ed25519.PublicKey)
return ok return ok
default:
return false
} }
return false
} }

View file

@ -7,7 +7,7 @@ import (
"reflect" "reflect"
"testing" "testing"
jose "github.com/go-jose/go-jose/v4" "gopkg.in/square/go-jose.v2"
) )
func TestFindKey(t *testing.T) { func TestFindKey(t *testing.T) {

View file

@ -1,53 +0,0 @@
//go:build !create_regression_data
package oidc
import (
"encoding/json"
"io"
"os"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Test_assert_regression verifies current output from
// json.Marshal to stored regression data.
// These tests are only ran when the create_regression_data
// tag is NOT set.
func Test_assert_regression(t *testing.T) {
buf := new(strings.Builder)
for _, obj := range regressionData {
name := jsonFilename(obj)
t.Run(name, func(t *testing.T) {
file, err := os.Open(name)
require.NoError(t, err)
defer file.Close()
_, err = io.Copy(buf, file)
require.NoError(t, err)
want := buf.String()
buf.Reset()
encodeJSON(t, buf, obj)
first := buf.String()
buf.Reset()
assert.JSONEq(t, want, first)
target := reflect.New(reflect.TypeOf(obj).Elem()).Interface()
require.NoError(t,
json.Unmarshal([]byte(first), target),
)
second, err := json.Marshal(target)
require.NoError(t, err)
assert.JSONEq(t, want, string(second))
})
}
}

View file

@ -1,24 +0,0 @@
//go:build create_regression_data
package oidc
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
// Test_create_regression generates the regression data.
// It is excluded from regular testing, unless
// called with the create_regression_data tag:
// go test -tags="create_regression_data" ./pkg/oidc
func Test_create_regression(t *testing.T) {
for _, obj := range regressionData {
file, err := os.Create(jsonFilename(obj))
require.NoError(t, err)
defer file.Close()
encodeJSON(t, file, obj)
}
}

View file

@ -1,23 +0,0 @@
{
"iss": "zitadel",
"sub": "hello@me.com",
"aud": [
"foo",
"bar"
],
"jti": "900",
"azp": "just@me.com",
"nonce": "6969",
"acr": "something",
"amr": [
"some",
"methods"
],
"scope": "email phone",
"client_id": "777",
"exp": 12345,
"iat": 12000,
"nbf": 12000,
"auth_time": 12000,
"foo": "bar"
}

View file

@ -1,51 +0,0 @@
{
"iss": "zitadel",
"aud": [
"foo",
"bar"
],
"jti": "900",
"azp": "just@me.com",
"nonce": "6969",
"at_hash": "acthashhash",
"c_hash": "hashhash",
"acr": "something",
"amr": [
"some",
"methods"
],
"sid": "666",
"client_id": "777",
"exp": 12345,
"iat": 12000,
"nbf": 12000,
"auth_time": 12000,
"address": {
"country": "Moon",
"formatted": "Sesame street 666\n666-666, Smallvile\nMoon",
"locality": "Smallvile",
"postal_code": "666-666",
"region": "Outer space",
"street_address": "Sesame street 666"
},
"birthdate": "1st of April",
"email": "tim@zitadel.com",
"email_verified": true,
"family_name": "Möhlmann",
"foo": "bar",
"gender": "male",
"given_name": "Tim",
"locale": "nl",
"middle_name": "Danger",
"name": "Tim Möhlmann",
"nickname": "muhlemmer",
"phone_number": "+1234567890",
"phone_number_verified": true,
"picture": "https://avatars.githubusercontent.com/u/5411563?v=4",
"preferred_username": "muhlemmer",
"profile": "https://github.com/muhlemmer",
"sub": "hello@me.com",
"updated_at": 1,
"website": "https://zitadel.com",
"zoneinfo": "Europe/Amsterdam"
}

View file

@ -1,44 +0,0 @@
{
"active": true,
"address": {
"country": "Moon",
"formatted": "Sesame street 666\n666-666, Smallvile\nMoon",
"locality": "Smallvile",
"postal_code": "666-666",
"region": "Outer space",
"street_address": "Sesame street 666"
},
"aud": [
"foo",
"bar"
],
"birthdate": "1st of April",
"client_id": "777",
"email": "tim@zitadel.com",
"email_verified": true,
"exp": 12345,
"family_name": "Möhlmann",
"foo": "bar",
"gender": "male",
"given_name": "Tim",
"iat": 12000,
"iss": "zitadel",
"jti": "900",
"locale": "nl",
"middle_name": "Danger",
"name": "Tim Möhlmann",
"nbf": 12000,
"nickname": "muhlemmer",
"phone_number": "+1234567890",
"phone_number_verified": true,
"picture": "https://avatars.githubusercontent.com/u/5411563?v=4",
"preferred_username": "muhlemmer",
"profile": "https://github.com/muhlemmer",
"scope": "email phone",
"sub": "hello@me.com",
"token_type": "idtoken",
"updated_at": 1,
"username": "muhlemmer",
"website": "https://zitadel.com",
"zoneinfo": "Europe/Amsterdam"
}

View file

@ -1,11 +0,0 @@
{
"aud": [
"foo",
"bar"
],
"exp": 12345,
"foo": "bar",
"iat": 12000,
"iss": "zitadel",
"sub": "hello@me.com"
}

View file

@ -1,30 +0,0 @@
{
"address": {
"country": "Moon",
"formatted": "Sesame street 666\n666-666, Smallvile\nMoon",
"locality": "Smallvile",
"postal_code": "666-666",
"region": "Outer space",
"street_address": "Sesame street 666"
},
"birthdate": "1st of April",
"email": "tim@zitadel.com",
"email_verified": true,
"family_name": "Möhlmann",
"foo": "bar",
"gender": "male",
"given_name": "Tim",
"locale": "nl",
"middle_name": "Danger",
"name": "Tim Möhlmann",
"nickname": "muhlemmer",
"phone_number": "+1234567890",
"phone_number_verified": true,
"picture": "https://avatars.githubusercontent.com/u/5411563?v=4",
"preferred_username": "muhlemmer",
"profile": "https://github.com/muhlemmer",
"sub": "hello@me.com",
"updated_at": 1,
"website": "https://zitadel.com",
"zoneinfo": "Europe/Amsterdam"
}

View file

@ -1,40 +0,0 @@
package oidc
// This file contains common functions and data for regression testing
import (
"encoding/json"
"fmt"
"io"
"path"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
const dataDir = "regression_data"
// jsonFilename builds a filename for the regression testdata.
// dataDir/<type_name>.json
func jsonFilename(obj any) string {
name := fmt.Sprintf("%T.json", obj)
return path.Join(
dataDir,
strings.TrimPrefix(name, "*"),
)
}
func encodeJSON(t *testing.T, w io.Writer, obj any) {
enc := json.NewEncoder(w)
enc.SetIndent("", "\t")
require.NoError(t, enc.Encode(obj))
}
var regressionData = []any{
accessTokenData,
idTokenData,
introspectionResponseData,
userInfoData,
jwtProfileAssertionData,
}

View file

@ -1,12 +1,10 @@
package oidc package oidc
// EndSessionRequest for the RP-Initiated Logout according to: // EndSessionRequest for the RP-Initiated Logout according to:
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout //https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
type EndSessionRequest struct { type EndSessionRequest struct {
IdTokenHint string `schema:"id_token_hint"` IdTokenHint string `schema:"id_token_hint"`
LogoutHint string `schema:"logout_hint"` ClientID string `schema:"client_id"`
ClientID string `schema:"client_id"` PostLogoutRedirectURI string `schema:"post_logout_redirect_uri"`
PostLogoutRedirectURI string `schema:"post_logout_redirect_uri"` State string `schema:"state"`
State string `schema:"state"`
UILocales Locales `schema:"ui_locales"`
} }

View file

@ -2,15 +2,15 @@ package oidc
import ( import (
"encoding/json" "encoding/json"
"os" "fmt"
"io/ioutil"
"time" "time"
jose "github.com/go-jose/go-jose/v4"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2"
"github.com/muhlemmer/gu" "github.com/zitadel/oidc/v2/pkg/crypto"
"github.com/zitadel/oidc/v2/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
) )
const ( const (
@ -20,226 +20,404 @@ const (
PrefixBearer = BearerToken + " " PrefixBearer = BearerToken + " "
) )
type Tokens[C IDClaims] struct { type Tokens struct {
*oauth2.Token *oauth2.Token
IDTokenClaims C IDTokenClaims IDTokenClaims
IDToken string IDToken string
} }
// TokenClaims contains the base Claims used all tokens. type AccessTokenClaims interface {
// It implements OpenID Connect Core 1.0, section 2. Claims
// https://openid.net/specs/openid-connect-core-1_0.html#IDToken GetSubject() string
// And RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens, GetTokenID() string
// section 2.2. https://datatracker.ietf.org/doc/html/rfc9068#name-data-structure SetPrivateClaims(map[string]interface{})
// GetClaims() map[string]interface{}
// TokenClaims implements the Claims interface,
// and can be used to extend larger claim types by embedding.
type TokenClaims struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Audience Audience `json:"aud,omitempty"`
Expiration Time `json:"exp,omitempty"`
IssuedAt Time `json:"iat,omitempty"`
AuthTime Time `json:"auth_time,omitempty"`
NotBefore Time `json:"nbf,omitempty"`
Nonce string `json:"nonce,omitempty"`
AuthenticationContextClassReference string `json:"acr,omitempty"`
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
AuthorizedParty string `json:"azp,omitempty"`
ClientID string `json:"client_id,omitempty"`
JWTID string `json:"jti,omitempty"`
Actor *ActorClaims `json:"act,omitempty"`
// Additional information set by this framework
SignatureAlg jose.SignatureAlgorithm `json:"-"`
} }
func (c *TokenClaims) GetIssuer() string { type IDTokenClaims interface {
return c.Issuer Claims
GetNotBefore() time.Time
GetJWTID() string
GetAccessTokenHash() string
GetCodeHash() string
GetAuthenticationMethodsReferences() []string
GetClientID() string
GetSignatureAlgorithm() jose.SignatureAlgorithm
SetAccessTokenHash(hash string)
SetUserinfo(userinfo UserInfo)
SetCodeHash(hash string)
UserInfo
} }
func (c *TokenClaims) GetSubject() string { func EmptyAccessTokenClaims() AccessTokenClaims {
return c.Subject return new(accessTokenClaims)
} }
func (c *TokenClaims) GetAudience() []string { func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id, clientID string, skew time.Duration) AccessTokenClaims {
return c.Audience
}
func (c *TokenClaims) GetExpiration() time.Time {
return c.Expiration.AsTime()
}
func (c *TokenClaims) GetIssuedAt() time.Time {
return c.IssuedAt.AsTime()
}
func (c *TokenClaims) GetNonce() string {
return c.Nonce
}
func (c *TokenClaims) GetAuthTime() time.Time {
return c.AuthTime.AsTime()
}
func (c *TokenClaims) GetAuthorizedParty() string {
return c.AuthorizedParty
}
func (c *TokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm {
return c.SignatureAlg
}
func (c *TokenClaims) GetAuthenticationContextClassReference() string {
return c.AuthenticationContextClassReference
}
func (c *TokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {
c.SignatureAlg = algorithm
}
type AccessTokenClaims struct {
TokenClaims
Scopes SpaceDelimitedArray `json:"scope,omitempty"`
Claims map[string]any `json:"-"`
}
func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration) *AccessTokenClaims {
now := time.Now().UTC().Add(-skew) now := time.Now().UTC().Add(-skew)
if len(audience) == 0 { if len(audience) == 0 {
audience = append(audience, clientID) audience = append(audience, clientID)
} }
return &AccessTokenClaims{ return &accessTokenClaims{
TokenClaims: TokenClaims{ Issuer: issuer,
Issuer: issuer, Subject: subject,
Subject: subject, Audience: audience,
Audience: audience, Expiration: Time(expiration),
Expiration: FromTime(expiration), IssuedAt: Time(now),
IssuedAt: FromTime(now), NotBefore: Time(now),
NotBefore: FromTime(now), JWTID: id,
ClientID: clientID,
JWTID: jwtid,
},
} }
} }
type atcAlias AccessTokenClaims type accessTokenClaims struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Audience Audience `json:"aud,omitempty"`
Expiration Time `json:"exp,omitempty"`
IssuedAt Time `json:"iat,omitempty"`
NotBefore Time `json:"nbf,omitempty"`
JWTID string `json:"jti,omitempty"`
AuthorizedParty string `json:"azp,omitempty"`
Nonce string `json:"nonce,omitempty"`
AuthTime Time `json:"auth_time,omitempty"`
CodeHash string `json:"c_hash,omitempty"`
AuthenticationContextClassReference string `json:"acr,omitempty"`
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
SessionID string `json:"sid,omitempty"`
Scopes []string `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
AccessTokenUseNumber int `json:"at_use_nbr,omitempty"`
func (a *AccessTokenClaims) MarshalJSON() ([]byte, error) { claims map[string]interface{} `json:"-"`
return mergeAndMarshalClaims((*atcAlias)(a), a.Claims) signatureAlg jose.SignatureAlgorithm `json:"-"`
} }
func (a *AccessTokenClaims) UnmarshalJSON(data []byte) error { // GetIssuer implements the Claims interface
return unmarshalJSONMulti(data, (*atcAlias)(a), &a.Claims) func (a *accessTokenClaims) GetIssuer() string {
return a.Issuer
} }
// IDTokenClaims extends TokenClaims by further implementing // GetAudience implements the Claims interface
// OpenID Connect Core 1.0, sections 3.1.3.6 (Code flow), func (a *accessTokenClaims) GetAudience() []string {
// 3.2.2.10 (implicit), 3.3.2.11 (Hybrid) and 5.1 (UserInfo). return a.Audience
// https://openid.net/specs/openid-connect-core-1_0.html#toc }
type IDTokenClaims struct {
TokenClaims // GetExpiration implements the Claims interface
NotBefore Time `json:"nbf,omitempty"` func (a *accessTokenClaims) GetExpiration() time.Time {
AccessTokenHash string `json:"at_hash,omitempty"` return time.Time(a.Expiration)
CodeHash string `json:"c_hash,omitempty"` }
SessionID string `json:"sid,omitempty"`
UserInfoProfile // GetIssuedAt implements the Claims interface
UserInfoEmail func (a *accessTokenClaims) GetIssuedAt() time.Time {
UserInfoPhone return time.Time(a.IssuedAt)
Address *UserInfoAddress `json:"address,omitempty"` }
Claims map[string]any `json:"-"`
// GetNonce implements the Claims interface
func (a *accessTokenClaims) GetNonce() string {
return a.Nonce
}
// GetAuthenticationContextClassReference implements the Claims interface
func (a *accessTokenClaims) GetAuthenticationContextClassReference() string {
return a.AuthenticationContextClassReference
}
// GetAuthTime implements the Claims interface
func (a *accessTokenClaims) GetAuthTime() time.Time {
return time.Time(a.AuthTime)
}
// GetAuthorizedParty implements the Claims interface
func (a *accessTokenClaims) GetAuthorizedParty() string {
return a.AuthorizedParty
}
// SetSignatureAlgorithm implements the Claims interface
func (a *accessTokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {
a.signatureAlg = algorithm
}
// GetSubject implements the AccessTokenClaims interface
func (a *accessTokenClaims) GetSubject() string {
return a.Subject
}
// GetTokenID implements the AccessTokenClaims interface
func (a *accessTokenClaims) GetTokenID() string {
return a.JWTID
}
// SetPrivateClaims implements the AccessTokenClaims interface
func (a *accessTokenClaims) SetPrivateClaims(claims map[string]interface{}) {
a.claims = claims
}
// GetClaims implements the AccessTokenClaims interface
func (a *accessTokenClaims) GetClaims() map[string]interface{} {
return a.claims
}
func (a *accessTokenClaims) MarshalJSON() ([]byte, error) {
type Alias accessTokenClaims
s := &struct {
*Alias
Expiration int64 `json:"exp,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
AuthTime int64 `json:"auth_time,omitempty"`
}{
Alias: (*Alias)(a),
}
if !time.Time(a.Expiration).IsZero() {
s.Expiration = time.Time(a.Expiration).Unix()
}
if !time.Time(a.IssuedAt).IsZero() {
s.IssuedAt = time.Time(a.IssuedAt).Unix()
}
if !time.Time(a.NotBefore).IsZero() {
s.NotBefore = time.Time(a.NotBefore).Unix()
}
if !time.Time(a.AuthTime).IsZero() {
s.AuthTime = time.Time(a.AuthTime).Unix()
}
b, err := json.Marshal(s)
if err != nil {
return nil, err
}
if a.claims == nil {
return b, nil
}
info, err := json.Marshal(a.claims)
if err != nil {
return nil, err
}
return http.ConcatenateJSON(b, info)
}
func (a *accessTokenClaims) UnmarshalJSON(data []byte) error {
type Alias accessTokenClaims
if err := json.Unmarshal(data, (*Alias)(a)); err != nil {
return err
}
claims := make(map[string]interface{})
if err := json.Unmarshal(data, &claims); err != nil {
return err
}
a.claims = claims
return nil
}
func EmptyIDTokenClaims() IDTokenClaims {
return new(idTokenClaims)
}
func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) IDTokenClaims {
audience = AppendClientIDToAudience(clientID, audience)
return &idTokenClaims{
Issuer: issuer,
Audience: audience,
Expiration: Time(expiration),
IssuedAt: Time(time.Now().UTC().Add(-skew)),
AuthTime: Time(authTime.Add(-skew)),
Nonce: nonce,
AuthenticationContextClassReference: acr,
AuthenticationMethodsReferences: amr,
AuthorizedParty: clientID,
UserInfo: &userinfo{Subject: subject},
}
}
type idTokenClaims struct {
Issuer string `json:"iss,omitempty"`
Audience Audience `json:"aud,omitempty"`
Expiration Time `json:"exp,omitempty"`
NotBefore Time `json:"nbf,omitempty"`
IssuedAt Time `json:"iat,omitempty"`
JWTID string `json:"jti,omitempty"`
AuthorizedParty string `json:"azp,omitempty"`
Nonce string `json:"nonce,omitempty"`
AuthTime Time `json:"auth_time,omitempty"`
AccessTokenHash string `json:"at_hash,omitempty"`
CodeHash string `json:"c_hash,omitempty"`
AuthenticationContextClassReference string `json:"acr,omitempty"`
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
ClientID string `json:"client_id,omitempty"`
UserInfo `json:"-"`
signatureAlg jose.SignatureAlgorithm
}
// GetIssuer implements the Claims interface
func (t *idTokenClaims) GetIssuer() string {
return t.Issuer
}
// GetAudience implements the Claims interface
func (t *idTokenClaims) GetAudience() []string {
return t.Audience
}
// GetExpiration implements the Claims interface
func (t *idTokenClaims) GetExpiration() time.Time {
return time.Time(t.Expiration)
}
// GetIssuedAt implements the Claims interface
func (t *idTokenClaims) GetIssuedAt() time.Time {
return time.Time(t.IssuedAt)
}
// GetNonce implements the Claims interface
func (t *idTokenClaims) GetNonce() string {
return t.Nonce
}
// GetAuthenticationContextClassReference implements the Claims interface
func (t *idTokenClaims) GetAuthenticationContextClassReference() string {
return t.AuthenticationContextClassReference
}
// GetAuthTime implements the Claims interface
func (t *idTokenClaims) GetAuthTime() time.Time {
return time.Time(t.AuthTime)
}
// GetAuthorizedParty implements the Claims interface
func (t *idTokenClaims) GetAuthorizedParty() string {
return t.AuthorizedParty
}
// SetSignatureAlgorithm implements the Claims interface
func (t *idTokenClaims) SetSignatureAlgorithm(alg jose.SignatureAlgorithm) {
t.signatureAlg = alg
}
// GetNotBefore implements the IDTokenClaims interface
func (t *idTokenClaims) GetNotBefore() time.Time {
return time.Time(t.NotBefore)
}
// GetJWTID implements the IDTokenClaims interface
func (t *idTokenClaims) GetJWTID() string {
return t.JWTID
} }
// GetAccessTokenHash implements the IDTokenClaims interface // GetAccessTokenHash implements the IDTokenClaims interface
func (t *IDTokenClaims) GetAccessTokenHash() string { func (t *idTokenClaims) GetAccessTokenHash() string {
return t.AccessTokenHash return t.AccessTokenHash
} }
func (t *IDTokenClaims) SetUserInfo(i *UserInfo) { // GetCodeHash implements the IDTokenClaims interface
t.Subject = i.Subject func (t *idTokenClaims) GetCodeHash() string {
t.UserInfoProfile = i.UserInfoProfile return t.CodeHash
t.UserInfoEmail = i.UserInfoEmail }
t.UserInfoPhone = i.UserInfoPhone
t.Address = i.Address // GetAuthenticationMethodsReferences implements the IDTokenClaims interface
if t.Claims == nil { func (t *idTokenClaims) GetAuthenticationMethodsReferences() []string {
t.Claims = make(map[string]any, len(t.Claims)) return t.AuthenticationMethodsReferences
}
// GetClientID implements the IDTokenClaims interface
func (t *idTokenClaims) GetClientID() string {
return t.ClientID
}
// GetSignatureAlgorithm implements the IDTokenClaims interface
func (t *idTokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm {
return t.signatureAlg
}
// SetAccessTokenHash implements the IDTokenClaims interface
func (t *idTokenClaims) SetAccessTokenHash(hash string) {
t.AccessTokenHash = hash
}
// SetUserinfo implements the IDTokenClaims interface
func (t *idTokenClaims) SetUserinfo(info UserInfo) {
t.UserInfo = info
}
// SetCodeHash implements the IDTokenClaims interface
func (t *idTokenClaims) SetCodeHash(hash string) {
t.CodeHash = hash
}
func (t *idTokenClaims) MarshalJSON() ([]byte, error) {
type Alias idTokenClaims
a := &struct {
*Alias
Expiration int64 `json:"exp,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
AuthTime int64 `json:"auth_time,omitempty"`
}{
Alias: (*Alias)(t),
} }
gu.MapMerge(i.Claims, t.Claims) if !time.Time(t.Expiration).IsZero() {
} a.Expiration = time.Time(t.Expiration).Unix()
func (t *IDTokenClaims) GetUserInfo() *UserInfo {
return &UserInfo{
Subject: t.Subject,
UserInfoProfile: t.UserInfoProfile,
UserInfoEmail: t.UserInfoEmail,
UserInfoPhone: t.UserInfoPhone,
Address: t.Address,
Claims: gu.MapCopy(t.Claims),
} }
} if !time.Time(t.IssuedAt).IsZero() {
a.IssuedAt = time.Time(t.IssuedAt).Unix()
func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) *IDTokenClaims {
audience = AppendClientIDToAudience(clientID, audience)
return &IDTokenClaims{
TokenClaims: TokenClaims{
Issuer: issuer,
Subject: subject,
Audience: audience,
Expiration: FromTime(expiration),
IssuedAt: FromTime(time.Now().Add(-skew)),
AuthTime: FromTime(authTime.Add(-skew)),
Nonce: nonce,
AuthenticationContextClassReference: acr,
AuthenticationMethodsReferences: amr,
AuthorizedParty: clientID,
ClientID: clientID,
},
} }
if !time.Time(t.NotBefore).IsZero() {
a.NotBefore = time.Time(t.NotBefore).Unix()
}
if !time.Time(t.AuthTime).IsZero() {
a.AuthTime = time.Time(t.AuthTime).Unix()
}
b, err := json.Marshal(a)
if err != nil {
return nil, err
}
if t.UserInfo == nil {
return b, nil
}
info, err := json.Marshal(t.UserInfo)
if err != nil {
return nil, err
}
return http.ConcatenateJSON(b, info)
} }
type itcAlias IDTokenClaims func (t *idTokenClaims) UnmarshalJSON(data []byte) error {
type Alias idTokenClaims
if err := json.Unmarshal(data, (*Alias)(t)); err != nil {
return err
}
userinfo := new(userinfo)
if err := json.Unmarshal(data, userinfo); err != nil {
return err
}
t.UserInfo = userinfo
func (i *IDTokenClaims) MarshalJSON() ([]byte, error) { return nil
return mergeAndMarshalClaims((*itcAlias)(i), i.Claims)
}
func (i *IDTokenClaims) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims)
}
// ActorClaims provides the `act` claims used for impersonation or delegation Token Exchange.
//
// An actor can be nested in case an obtained token is used as actor token to obtain impersonation or delegation.
// This allows creating a chain of actors.
// See [RFC 8693, section 4.1](https://www.rfc-editor.org/rfc/rfc8693#name-act-actor-claim).
type ActorClaims struct {
Actor *ActorClaims `json:"act,omitempty"`
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Claims map[string]any `json:"-"`
}
type acAlias ActorClaims
func (c *ActorClaims) MarshalJSON() ([]byte, error) {
return mergeAndMarshalClaims((*acAlias)(c), c.Claims)
}
func (c *ActorClaims) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*acAlias)(c), &c.Claims)
} }
type AccessTokenResponse struct { type AccessTokenResponse struct {
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"` AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"` TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"` RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"` ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"` IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
State string `json:"state,omitempty" schema:"state,omitempty"` State string `json:"state,omitempty" schema:"state,omitempty"`
Scope SpaceDelimitedArray `json:"scope,omitempty" schema:"scope,omitempty"`
} }
type JWTProfileAssertionClaims struct { type JWTProfileAssertionClaims interface {
GetKeyID() string
GetPrivateKey() []byte
GetIssuer() string
GetSubject() string
GetAudience() []string
GetExpiration() time.Time
GetIssuedAt() time.Time
SetCustomClaim(key string, value interface{})
GetCustomClaim(key string) interface{}
}
type jwtProfileAssertion struct {
PrivateKeyID string `json:"-"` PrivateKeyID string `json:"-"`
PrivateKey []byte `json:"-"` PrivateKey []byte `json:"-"`
Issuer string `json:"iss"` Issuer string `json:"iss"`
@ -248,21 +426,91 @@ type JWTProfileAssertionClaims struct {
Expiration Time `json:"exp"` Expiration Time `json:"exp"`
IssuedAt Time `json:"iat"` IssuedAt Time `json:"iat"`
Claims map[string]any `json:"-"` customClaims map[string]interface{}
} }
type jpaAlias JWTProfileAssertionClaims func (j *jwtProfileAssertion) MarshalJSON() ([]byte, error) {
type Alias jwtProfileAssertion
a := (*Alias)(j)
func (j *JWTProfileAssertionClaims) MarshalJSON() ([]byte, error) { b, err := json.Marshal(a)
return mergeAndMarshalClaims((*jpaAlias)(j), j.Claims) if err != nil {
return nil, err
}
if len(j.customClaims) == 0 {
return b, nil
}
err = json.Unmarshal(b, &j.customClaims)
if err != nil {
return nil, fmt.Errorf("jws: invalid map of custom claims %v", j.customClaims)
}
return json.Marshal(j.customClaims)
} }
func (j *JWTProfileAssertionClaims) UnmarshalJSON(data []byte) error { func (j *jwtProfileAssertion) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*jpaAlias)(j), &j.Claims) type Alias jwtProfileAssertion
a := (*Alias)(j)
err := json.Unmarshal(data, a)
if err != nil {
return err
}
err = json.Unmarshal(data, &j.customClaims)
if err != nil {
return err
}
return nil
} }
func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (*JWTProfileAssertionClaims, error) { func (j *jwtProfileAssertion) GetKeyID() string {
data, err := os.ReadFile(filename) return j.PrivateKeyID
}
func (j *jwtProfileAssertion) GetPrivateKey() []byte {
return j.PrivateKey
}
func (j *jwtProfileAssertion) SetCustomClaim(key string, value interface{}) {
if j.customClaims == nil {
j.customClaims = make(map[string]interface{})
}
j.customClaims[key] = value
}
func (j *jwtProfileAssertion) GetCustomClaim(key string) interface{} {
if j.customClaims == nil {
return nil
}
return j.customClaims[key]
}
func (j *jwtProfileAssertion) GetIssuer() string {
return j.Issuer
}
func (j *jwtProfileAssertion) GetSubject() string {
return j.Subject
}
func (j *jwtProfileAssertion) GetAudience() []string {
return j.Audience
}
func (j *jwtProfileAssertion) GetExpiration() time.Time {
return time.Time(j.Expiration)
}
func (j *jwtProfileAssertion) GetIssuedAt() time.Time {
return time.Time(j.IssuedAt)
}
func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) {
data, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -282,19 +530,19 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string, op
return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...)) return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...))
} }
func JWTProfileDelegatedSubject(sub string) func(*JWTProfileAssertionClaims) { func JWTProfileDelegatedSubject(sub string) func(*jwtProfileAssertion) {
return func(j *JWTProfileAssertionClaims) { return func(j *jwtProfileAssertion) {
j.Subject = sub j.Subject = sub
} }
} }
func JWTProfileCustomClaim(key string, value any) func(*JWTProfileAssertionClaims) { func JWTProfileCustomClaim(key string, value interface{}) func(*jwtProfileAssertion) {
return func(j *JWTProfileAssertionClaims) { return func(j *jwtProfileAssertion) {
j.Claims[key] = value j.customClaims[key] = value
} }
} }
func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (*JWTProfileAssertionClaims, error) { func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) {
keyData := new(struct { keyData := new(struct {
KeyID string `json:"keyId"` KeyID string `json:"keyId"`
Key string `json:"key"` Key string `json:"key"`
@ -307,18 +555,18 @@ func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...
return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...), nil return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...), nil
} }
type AssertionOption func(*JWTProfileAssertionClaims) type AssertionOption func(*jwtProfileAssertion)
func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) *JWTProfileAssertionClaims { func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) JWTProfileAssertionClaims {
j := &JWTProfileAssertionClaims{ j := &jwtProfileAssertion{
PrivateKey: key, PrivateKey: key,
PrivateKeyID: keyID, PrivateKeyID: keyID,
Issuer: userID, Issuer: userID,
Subject: userID, Subject: userID,
IssuedAt: FromTime(time.Now().UTC()), IssuedAt: Time(time.Now().UTC()),
Expiration: FromTime(time.Now().Add(1 * time.Hour).UTC()), Expiration: Time(time.Now().Add(1 * time.Hour).UTC()),
Audience: audience, Audience: audience,
Claims: make(map[string]any), customClaims: make(map[string]interface{}),
} }
for _, opt := range opts { for _, opt := range opts {
@ -346,14 +594,14 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
return append(audience, clientID) return append(audience, clientID)
} }
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) { func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error) {
privateKey, algorithm, err := crypto.BytesToPrivateKey(assertion.PrivateKey) privateKey, err := crypto.BytesToPrivateKey(assertion.GetPrivateKey())
if err != nil { if err != nil {
return "", err return "", err
} }
key := jose.SigningKey{ key := jose.SigningKey{
Algorithm: algorithm, Algorithm: jose.RS256,
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.GetKeyID()},
} }
signer, err := jose.NewSigner(key, &jose.SignerOptions{}) signer, err := jose.NewSigner(key, &jose.SignerOptions{})
if err != nil { if err != nil {
@ -378,45 +626,4 @@ type TokenExchangeResponse struct {
ExpiresIn uint64 `json:"expires_in,omitempty"` ExpiresIn uint64 `json:"expires_in,omitempty"`
Scopes SpaceDelimitedArray `json:"scope,omitempty"` Scopes SpaceDelimitedArray `json:"scope,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"` RefreshToken string `json:"refresh_token,omitempty"`
// IDToken field allows returning an additional ID token
// if the requested_token_type was Access Token and scope contained openid.
IDToken string `json:"id_token,omitempty"`
}
type LogoutTokenClaims struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Audience Audience `json:"aud,omitempty"`
IssuedAt Time `json:"iat,omitempty"`
Expiration Time `json:"exp,omitempty"`
JWTID string `json:"jti,omitempty"`
Events map[string]any `json:"events,omitempty"`
SessionID string `json:"sid,omitempty"`
Claims map[string]any `json:"-"`
}
type ltcAlias LogoutTokenClaims
func (i *LogoutTokenClaims) MarshalJSON() ([]byte, error) {
return mergeAndMarshalClaims((*ltcAlias)(i), i.Claims)
}
func (i *LogoutTokenClaims) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*ltcAlias)(i), &i.Claims)
}
func NewLogoutTokenClaims(issuer, subject string, audience Audience, expiration time.Time, jwtID, sessionID string, skew time.Duration) *LogoutTokenClaims {
return &LogoutTokenClaims{
Issuer: issuer,
Subject: subject,
Audience: audience,
IssuedAt: FromTime(time.Now().Add(-skew)),
Expiration: FromTime(expiration),
JWTID: jwtID,
Events: map[string]any{
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
},
SessionID: sessionID,
}
} }

View file

@ -3,10 +3,9 @@ package oidc
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"slices"
"time" "time"
jose "github.com/go-jose/go-jose/v4" "gopkg.in/square/go-jose.v2"
) )
const ( const (
@ -58,7 +57,13 @@ var AllTokenTypes = []TokenType{
type TokenType string type TokenType string
func (t TokenType) IsSupported() bool { func (t TokenType) IsSupported() bool {
return slices.Contains(AllTokenTypes, t) for _, tt := range AllTokenTypes {
if t == tt {
return true
}
}
return false
} }
type TokenRequest interface { type TokenRequest interface {
@ -72,10 +77,10 @@ type AccessTokenRequest struct {
Code string `schema:"code"` Code string `schema:"code"`
RedirectURI string `schema:"redirect_uri"` RedirectURI string `schema:"redirect_uri"`
ClientID string `schema:"client_id"` ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret,omitempty"` ClientSecret string `schema:"client_secret"`
CodeVerifier string `schema:"code_verifier,omitempty"` CodeVerifier string `schema:"code_verifier"`
ClientAssertion string `schema:"client_assertion,omitempty"` ClientAssertion string `schema:"client_assertion"`
ClientAssertionType string `schema:"client_assertion_type,omitempty"` ClientAssertionType string `schema:"client_assertion_type"`
} }
func (a *AccessTokenRequest) GrantType() GrantType { func (a *AccessTokenRequest) GrantType() GrantType {
@ -125,7 +130,7 @@ type JWTTokenRequest struct {
IssuedAt Time `json:"iat"` IssuedAt Time `json:"iat"`
ExpiresAt Time `json:"exp"` ExpiresAt Time `json:"exp"`
private map[string]any private map[string]interface{}
} }
func (j *JWTTokenRequest) MarshalJSON() ([]byte, error) { func (j *JWTTokenRequest) MarshalJSON() ([]byte, error) {
@ -166,7 +171,7 @@ func (j *JWTTokenRequest) UnmarshalJSON(data []byte) error {
return nil return nil
} }
func (j *JWTTokenRequest) GetCustomClaim(key string) any { func (j *JWTTokenRequest) GetCustomClaim(key string) interface{} {
return j.private[key] return j.private[key]
} }
@ -182,12 +187,12 @@ func (j *JWTTokenRequest) GetAudience() []string {
// GetExpiration implements the Claims interface // GetExpiration implements the Claims interface
func (j *JWTTokenRequest) GetExpiration() time.Time { func (j *JWTTokenRequest) GetExpiration() time.Time {
return j.ExpiresAt.AsTime() return time.Time(j.ExpiresAt)
} }
// GetIssuedAt implements the Claims interface // GetIssuedAt implements the Claims interface
func (j *JWTTokenRequest) GetIssuedAt() time.Time { func (j *JWTTokenRequest) GetIssuedAt() time.Time {
return j.IssuedAt.AsTime() return time.Time(j.IssuedAt)
} }
// GetNonce implements the Claims interface // GetNonce implements the Claims interface
@ -236,7 +241,7 @@ type TokenExchangeRequest struct {
} }
type ClientCredentialsRequest struct { type ClientCredentialsRequest struct {
GrantType GrantType `schema:"grant_type,omitempty"` GrantType GrantType `schema:"grant_type"`
Scope SpaceDelimitedArray `schema:"scope"` Scope SpaceDelimitedArray `schema:"scope"`
ClientID string `schema:"client_id"` ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"` ClientSecret string `schema:"client_secret"`

View file

@ -1,280 +0,0 @@
package oidc
import (
"testing"
"time"
jose "github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
)
var (
tokenClaimsData = TokenClaims{
Issuer: "zitadel",
Subject: "hello@me.com",
Audience: Audience{"foo", "bar"},
Expiration: 12345,
IssuedAt: 12000,
JWTID: "900",
AuthorizedParty: "just@me.com",
Nonce: "6969",
AuthTime: 12000,
NotBefore: 12000,
AuthenticationContextClassReference: "something",
AuthenticationMethodsReferences: []string{"some", "methods"},
ClientID: "777",
SignatureAlg: jose.ES256,
}
accessTokenData = &AccessTokenClaims{
TokenClaims: tokenClaimsData,
Scopes: []string{"email", "phone"},
Claims: map[string]any{
"foo": "bar",
},
}
idTokenData = &IDTokenClaims{
TokenClaims: tokenClaimsData,
NotBefore: 12000,
AccessTokenHash: "acthashhash",
CodeHash: "hashhash",
SessionID: "666",
UserInfoProfile: userInfoData.UserInfoProfile,
UserInfoEmail: userInfoData.UserInfoEmail,
UserInfoPhone: userInfoData.UserInfoPhone,
Address: userInfoData.Address,
Claims: map[string]any{
"foo": "bar",
},
}
introspectionResponseData = &IntrospectionResponse{
Active: true,
Scope: SpaceDelimitedArray{"email", "phone"},
ClientID: "777",
TokenType: "idtoken",
Expiration: 12345,
IssuedAt: 12000,
NotBefore: 12000,
Subject: "hello@me.com",
Audience: Audience{"foo", "bar"},
Issuer: "zitadel",
JWTID: "900",
Username: "muhlemmer",
UserInfoProfile: userInfoData.UserInfoProfile,
UserInfoEmail: userInfoData.UserInfoEmail,
UserInfoPhone: userInfoData.UserInfoPhone,
Address: userInfoData.Address,
Claims: map[string]any{
"foo": "bar",
},
}
userInfoData = &UserInfo{
Subject: "hello@me.com",
UserInfoProfile: UserInfoProfile{
Name: "Tim Möhlmann",
GivenName: "Tim",
FamilyName: "Möhlmann",
MiddleName: "Danger",
Nickname: "muhlemmer",
Profile: "https://github.com/muhlemmer",
Picture: "https://avatars.githubusercontent.com/u/5411563?v=4",
Website: "https://zitadel.com",
Gender: "male",
Birthdate: "1st of April",
Zoneinfo: "Europe/Amsterdam",
Locale: NewLocale(language.Dutch),
UpdatedAt: 1,
PreferredUsername: "muhlemmer",
},
UserInfoEmail: UserInfoEmail{
Email: "tim@zitadel.com",
EmailVerified: true,
},
UserInfoPhone: UserInfoPhone{
PhoneNumber: "+1234567890",
PhoneNumberVerified: true,
},
Address: &UserInfoAddress{
Formatted: "Sesame street 666\n666-666, Smallvile\nMoon",
StreetAddress: "Sesame street 666",
Locality: "Smallvile",
Region: "Outer space",
PostalCode: "666-666",
Country: "Moon",
},
Claims: map[string]any{
"foo": "bar",
},
}
jwtProfileAssertionData = &JWTProfileAssertionClaims{
PrivateKeyID: "8888",
PrivateKey: []byte("qwerty"),
Issuer: "zitadel",
Subject: "hello@me.com",
Audience: Audience{"foo", "bar"},
Expiration: 12345,
IssuedAt: 12000,
Claims: map[string]any{
"foo": "bar",
},
}
)
func TestTokenClaims(t *testing.T) {
claims := tokenClaimsData
assert.Equal(t, claims.Issuer, tokenClaimsData.GetIssuer())
assert.Equal(t, claims.Subject, tokenClaimsData.GetSubject())
assert.Equal(t, []string(claims.Audience), tokenClaimsData.GetAudience())
assert.Equal(t, claims.Expiration.AsTime(), tokenClaimsData.GetExpiration())
assert.Equal(t, claims.IssuedAt.AsTime(), tokenClaimsData.GetIssuedAt())
assert.Equal(t, claims.Nonce, tokenClaimsData.GetNonce())
assert.Equal(t, claims.AuthTime.AsTime(), tokenClaimsData.GetAuthTime())
assert.Equal(t, claims.AuthorizedParty, tokenClaimsData.GetAuthorizedParty())
assert.Equal(t, claims.SignatureAlg, tokenClaimsData.GetSignatureAlgorithm())
assert.Equal(t, claims.AuthenticationContextClassReference, tokenClaimsData.GetAuthenticationContextClassReference())
claims.SetSignatureAlgorithm(jose.ES384)
assert.Equal(t, jose.ES384, claims.SignatureAlg)
}
func TestNewAccessTokenClaims(t *testing.T) {
want := &AccessTokenClaims{
TokenClaims: TokenClaims{
Issuer: "zitadel",
Subject: "hello@me.com",
Audience: Audience{"foo"},
Expiration: 12345,
ClientID: "foo",
JWTID: "900",
},
}
got := NewAccessTokenClaims(
want.Issuer, want.Subject, nil,
want.Expiration.AsTime(), want.JWTID, "foo", time.Second,
)
// test if the dynamic timestamps are around now,
// allowing for a delta of 1, just in case we flip on
// either side of a second boundry.
nowMinusSkew := NowTime() - 1
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
assert.InDelta(t, int64(nowMinusSkew), int64(got.NotBefore), 1)
// Make equal not fail on dynamic timestamp
got.IssuedAt = 0
got.NotBefore = 0
assert.Equal(t, want, got)
}
func TestIDTokenClaims_GetAccessTokenHash(t *testing.T) {
assert.Equal(t, idTokenData.AccessTokenHash, idTokenData.GetAccessTokenHash())
}
func TestIDTokenClaims_SetUserInfo(t *testing.T) {
want := IDTokenClaims{
TokenClaims: TokenClaims{
Subject: userInfoData.Subject,
},
UserInfoProfile: userInfoData.UserInfoProfile,
UserInfoEmail: userInfoData.UserInfoEmail,
UserInfoPhone: userInfoData.UserInfoPhone,
Address: userInfoData.Address,
Claims: map[string]any{
"foo": "bar",
},
}
var got IDTokenClaims
got.SetUserInfo(userInfoData)
assert.Equal(t, want, got)
}
func TestNewIDTokenClaims(t *testing.T) {
want := &IDTokenClaims{
TokenClaims: TokenClaims{
Issuer: "zitadel",
Subject: "hello@me.com",
Audience: Audience{"foo", "just@me.com"},
Expiration: 12345,
AuthTime: 12000,
Nonce: "6969",
AuthenticationContextClassReference: "something",
AuthenticationMethodsReferences: []string{"some", "methods"},
AuthorizedParty: "just@me.com",
ClientID: "just@me.com",
},
}
got := NewIDTokenClaims(
want.Issuer, want.Subject, want.Audience,
want.Expiration.AsTime(),
want.AuthTime.AsTime().Add(time.Second),
want.Nonce, want.AuthenticationContextClassReference,
want.AuthenticationMethodsReferences, want.AuthorizedParty,
time.Second,
)
// test if the dynamic timestamp is around now,
// allowing for a delta of 1, just in case we flip on
// either side of a second boundry.
nowMinusSkew := NowTime() - 1
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
// Make equal not fail on dynamic timestamp
got.IssuedAt = 0
assert.Equal(t, want, got)
}
func TestIDTokenClaims_GetUserInfo(t *testing.T) {
want := &UserInfo{
Subject: idTokenData.Subject,
UserInfoProfile: idTokenData.UserInfoProfile,
UserInfoEmail: idTokenData.UserInfoEmail,
UserInfoPhone: idTokenData.UserInfoPhone,
Address: idTokenData.Address,
Claims: idTokenData.Claims,
}
got := idTokenData.GetUserInfo()
assert.Equal(t, want, got)
}
func TestNewLogoutTokenClaims(t *testing.T) {
want := &LogoutTokenClaims{
Issuer: "zitadel",
Subject: "hello@me.com",
Audience: Audience{"foo", "just@me.com"},
Expiration: 12345,
JWTID: "jwtID",
Events: map[string]any{
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
},
SessionID: "sessionID",
Claims: nil,
}
got := NewLogoutTokenClaims(
want.Issuer,
want.Subject,
want.Audience,
want.Expiration.AsTime(),
want.JWTID,
want.SessionID,
1*time.Second,
)
// test if the dynamic timestamp is around now,
// allowing for a delta of 1, just in case we flip on
// either side of a second boundry.
nowMinusSkew := NowTime() - 1
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
// Make equal not fail on dynamic timestamp
got.IssuedAt = 0
assert.Equal(t, want, got)
}

View file

@ -3,28 +3,26 @@ package oidc
import ( import (
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
"time" "time"
jose "github.com/go-jose/go-jose/v4" "github.com/gorilla/schema"
"github.com/muhlemmer/gu"
"github.com/zitadel/schema"
"golang.org/x/text/language" "golang.org/x/text/language"
"gopkg.in/square/go-jose.v2"
) )
type Audience []string type Audience []string
func (a *Audience) UnmarshalJSON(text []byte) error { func (a *Audience) UnmarshalJSON(text []byte) error {
var i any var i interface{}
err := json.Unmarshal(text, &i) err := json.Unmarshal(text, &i)
if err != nil { if err != nil {
return err return err
} }
switch aud := i.(type) { switch aud := i.(type) {
case []any: case []interface{}:
*a = make([]string, len(aud)) *a = make([]string, len(aud))
for i, audience := range aud { for i, audience := range aud {
(*a)[i] = audience.(string) (*a)[i] = audience.(string)
@ -35,17 +33,6 @@ func (a *Audience) UnmarshalJSON(text []byte) error {
return nil return nil
} }
func (a *Audience) MarshalJSON() ([]byte, error) {
len := len(*a)
if len > 1 {
return json.Marshal(*a)
} else if len == 1 {
return json.Marshal((*a)[0])
}
return nil, errors.New("aud is empty")
}
type Display string type Display string
func (d *Display) UnmarshalText(text []byte) error { func (d *Display) UnmarshalText(text []byte) error {
@ -59,119 +46,16 @@ func (d *Display) UnmarshalText(text []byte) error {
type Gender string type Gender string
type Locale struct {
tag language.Tag
}
func NewLocale(tag language.Tag) *Locale {
return &Locale{tag: tag}
}
func (l *Locale) Tag() language.Tag {
if l == nil {
return language.Und
}
return l.tag
}
func (l *Locale) String() string {
return l.Tag().String()
}
func (l *Locale) MarshalJSON() ([]byte, error) {
tag := l.Tag()
if tag.IsRoot() {
return []byte("null"), nil
}
return json.Marshal(tag)
}
// UnmarshalJSON implements json.Unmarshaler.
// When [language.ValueError] is encountered, the containing tag will be set
// to an empty value (language "und") and no error will be returned.
// This state can be checked with the `l.Tag().IsRoot()` method.
func (l *Locale) UnmarshalJSON(data []byte) error {
if len(data) == 0 || string(data) == "\"\"" {
return nil
}
err := json.Unmarshal(data, &l.tag)
if err == nil {
return nil
}
// catch "well-formed but unknown" errors
var target language.ValueError
if errors.As(err, &target) {
l.tag = language.Tag{}
return nil
}
return err
}
type Locales []language.Tag type Locales []language.Tag
// ParseLocales parses a slice of strings into Locales. func (l *Locales) UnmarshalText(text []byte) error {
// If an entry causes a parse error or is undefined, locales := strings.Split(string(text), " ")
// it is ignored and not set to Locales.
func ParseLocales(locales []string) Locales {
out := make(Locales, 0, len(locales))
for _, locale := range locales { for _, locale := range locales {
tag, err := language.Parse(locale) tag, err := language.Parse(locale)
if err == nil && !tag.IsRoot() { if err == nil && !tag.IsRoot() {
out = append(out, tag) *l = append(*l, tag)
} }
} }
return out
}
func (l Locales) String() string {
tags := make([]string, len(l))
for i, tag := range l {
tags[i] = tag.String()
}
return strings.Join(tags, " ")
}
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
// It decodes an unquoted space seperated string into Locales.
// Undefined language tags in the input are ignored and ommited from
// the resulting Locales.
func (l *Locales) UnmarshalText(text []byte) error {
*l = ParseLocales(
strings.Split(string(text), " "),
)
return nil
}
// UnmarshalJSON implements the [json.Unmarshaler] interface.
// It decodes a json array or a space seperated string into Locales.
// Undefined language tags in the input are ignored and ommited from
// the resulting Locales.
func (l *Locales) UnmarshalJSON(data []byte) error {
var dst any
if err := json.Unmarshal(data, &dst); err != nil {
return fmt.Errorf("oidc locales: %w", err)
}
// We catch the posibility of a space seperated string here,
// because UnmarshalText might have been implicetely called
// by the json library before we added UnmarshalJSON.
switch v := dst.(type) {
case nil:
*l = nil
case string:
*l = ParseLocales(strings.Split(v, " "))
case []any:
locales, err := gu.AssertInterfaces[string](v)
if err != nil {
return fmt.Errorf("oidc locales: %w", err)
}
*l = ParseLocales(locales)
default:
return fmt.Errorf("oidc locales: unsupported type: %T", v)
}
return nil return nil
} }
@ -189,7 +73,7 @@ type ResponseType string
type ResponseMode string type ResponseMode string
func (s SpaceDelimitedArray) String() string { func (s SpaceDelimitedArray) Encode() string {
return strings.Join(s, " ") return strings.Join(s, " ")
} }
@ -199,11 +83,11 @@ func (s *SpaceDelimitedArray) UnmarshalText(text []byte) error {
} }
func (s SpaceDelimitedArray) MarshalText() ([]byte, error) { func (s SpaceDelimitedArray) MarshalText() ([]byte, error) {
return []byte(s.String()), nil return []byte(s.Encode()), nil
} }
func (s SpaceDelimitedArray) MarshalJSON() ([]byte, error) { func (s SpaceDelimitedArray) MarshalJSON() ([]byte, error) {
return json.Marshal((s).String()) return json.Marshal((s).Encode())
} }
func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error { func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error {
@ -215,7 +99,7 @@ func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error {
return nil return nil
} }
func (s *SpaceDelimitedArray) Scan(src any) error { func (s *SpaceDelimitedArray) Scan(src interface{}) error {
if src == nil { if src == nil {
*s = nil *s = nil
return nil return nil
@ -248,58 +132,26 @@ func (s SpaceDelimitedArray) Value() (driver.Value, error) {
func NewEncoder() *schema.Encoder { func NewEncoder() *schema.Encoder {
e := schema.NewEncoder() e := schema.NewEncoder()
e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string { e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string {
return value.Interface().(SpaceDelimitedArray).String() return value.Interface().(SpaceDelimitedArray).Encode()
})
e.RegisterEncoder(Locales{}, func(value reflect.Value) string {
return value.Interface().(Locales).String()
}) })
return e return e
} }
type Time int64 type Time time.Time
func (ts Time) AsTime() time.Time { func (t *Time) UnmarshalJSON(data []byte) error {
if ts == 0 { var i int64
return time.Time{} if err := json.Unmarshal(data, &i); err != nil {
} return err
return time.Unix(int64(ts), 0)
}
func FromTime(tt time.Time) Time {
if tt.IsZero() {
return 0
}
return Time(tt.Unix())
}
func NowTime() Time {
return FromTime(time.Now())
}
func (ts *Time) UnmarshalJSON(data []byte) error {
var v any
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf("oidc.Time: %w", err)
}
switch x := v.(type) {
case float64:
*ts = Time(x)
case string:
// Compatibility with Auth0:
// https://github.com/zitadel/oidc/issues/292
tt, err := time.Parse(time.RFC3339, x)
if err != nil {
return fmt.Errorf("oidc.Time: %w", err)
}
*ts = FromTime(tt)
case nil:
*ts = 0
default:
return fmt.Errorf("oidc.Time: unable to parse type %T with value %v", x, x)
} }
*t = Time(time.Unix(i, 0).UTC())
return nil return nil
} }
func (t *Time) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(*t).UTC().Unix())
}
type RequestObject struct { type RequestObject struct {
Issuer string `json:"iss"` Issuer string `json:"iss"`
Audience Audience `json:"aud"` Audience Audience `json:"aud"`
@ -310,4 +162,5 @@ func (r *RequestObject) GetIssuer() string {
return r.Issuer return r.Issuer
} }
func (*RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {} func (r *RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {
}

View file

@ -7,11 +7,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"time"
"github.com/gorilla/schema"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/schema"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
@ -113,175 +111,6 @@ func TestDisplay_UnmarshalText(t *testing.T) {
} }
} }
func TestLocale_Tag(t *testing.T) {
tests := []struct {
name string
l *Locale
want language.Tag
}{
{
name: "nil",
l: nil,
want: language.Und,
},
{
name: "Und",
l: NewLocale(language.Und),
want: language.Und,
},
{
name: "language",
l: NewLocale(language.Afrikaans),
want: language.Afrikaans,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.l.Tag())
})
}
}
func TestLocale_String(t *testing.T) {
tests := []struct {
name string
l *Locale
want language.Tag
}{
{
name: "nil",
l: nil,
want: language.Und,
},
{
name: "Und",
l: NewLocale(language.Und),
want: language.Und,
},
{
name: "language",
l: NewLocale(language.Afrikaans),
want: language.Afrikaans,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want.String(), tt.l.String())
})
}
}
func TestLocale_MarshalJSON(t *testing.T) {
tests := []struct {
name string
l *Locale
want string
wantErr bool
}{
{
name: "nil",
l: nil,
want: "null",
},
{
name: "und",
l: NewLocale(language.Und),
want: "null",
},
{
name: "language",
l: NewLocale(language.Afrikaans),
want: `"af"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := json.Marshal(tt.l)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want, string(got))
})
}
}
func TestLocale_UnmarshalJSON(t *testing.T) {
type dst struct {
Locale *Locale `json:"locale,omitempty"`
}
tests := []struct {
name string
input string
want dst
wantErr bool
}{
{
name: "value not present",
input: `{}`,
wantErr: false,
want: dst{
Locale: nil,
},
},
{
name: "null",
input: `{"locale": null}`,
wantErr: false,
want: dst{
Locale: nil,
},
},
{
name: "empty, ignored",
input: `{"locale": ""}`,
wantErr: false,
want: dst{
Locale: &Locale{},
},
},
{
name: "afrikaans, ok",
input: `{"locale": "af"}`,
want: dst{
Locale: NewLocale(language.Afrikaans),
},
},
{
name: "gb, ignored",
input: `{"locale": "gb"}`,
want: dst{
Locale: &Locale{},
},
},
{
name: "bad form, error",
input: `{"locale": "g!!!!!"}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got dst
err := json.Unmarshal([]byte(tt.input), &got)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestParseLocales(t *testing.T) {
in := []string{language.Afrikaans.String(), language.Danish.String(), "foobar", language.Und.String()}
want := Locales{language.Afrikaans, language.Danish}
got := ParseLocales(in)
assert.ElementsMatch(t, want, got)
}
func TestLocales_UnmarshalText(t *testing.T) { func TestLocales_UnmarshalText(t *testing.T) {
type args struct { type args struct {
text []byte text []byte
@ -339,80 +168,6 @@ func TestLocales_UnmarshalText(t *testing.T) {
} }
} }
func TestLocales_UnmarshalJSON(t *testing.T) {
in := []string{language.Afrikaans.String(), language.Danish.String(), "foobar", language.Und.String()}
spaceSepStr := strconv.Quote(strings.Join(in, " "))
jsonArray, err := json.Marshal(in)
require.NoError(t, err)
out := Locales{language.Afrikaans, language.Danish}
type args struct {
data []byte
}
tests := []struct {
name string
args args
want Locales
wantErr bool
}{
{
name: "invalid JSON",
args: args{
data: []byte("~~~"),
},
wantErr: true,
},
{
name: "null",
args: args{
data: []byte("null"),
},
want: nil,
},
{
name: "space seperated string",
args: args{
data: []byte(spaceSepStr),
},
want: out,
},
{
name: "json string array",
args: args{
data: jsonArray,
},
want: out,
},
{
name: "json invalid array",
args: args{
data: []byte(`[1,2,3]`),
},
wantErr: true,
},
{
name: "invalid type (float64)",
args: args{
data: []byte("22"),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got Locales
err := got.UnmarshalJSON([]byte(tt.args.data))
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestScopes_UnmarshalText(t *testing.T) { func TestScopes_UnmarshalText(t *testing.T) {
type args struct { type args struct {
text []byte text []byte
@ -599,107 +354,3 @@ func TestNewEncoder(t *testing.T) {
schema.NewDecoder().Decode(&b, values) schema.NewDecoder().Decode(&b, values)
assert.Equal(t, a, b) assert.Equal(t, a, b)
} }
func TestTime_AsTime(t *testing.T) {
tests := []struct {
name string
ts Time
want time.Time
}{
{
name: "unset",
ts: 0,
want: time.Time{},
},
{
name: "set",
ts: 1,
want: time.Unix(1, 0),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.ts.AsTime()
assert.Equal(t, tt.want, got)
})
}
}
func TestTime_FromTime(t *testing.T) {
tests := []struct {
name string
tt time.Time
want Time
}{
{
name: "zero",
tt: time.Time{},
want: 0,
},
{
name: "set",
tt: time.Unix(1, 0),
want: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FromTime(tt.tt)
assert.Equal(t, tt.want, got)
})
}
}
func TestTime_UnmarshalJSON(t *testing.T) {
type dst struct {
UpdatedAt Time `json:"updated_at"`
}
tests := []struct {
name string
json string
want dst
wantErr bool
}{
{
name: "RFC3339", // https://github.com/zitadel/oidc/issues/292
json: `{"updated_at": "2021-05-11T21:13:25.566Z"}`,
want: dst{UpdatedAt: 1620767605},
},
{
name: "int",
json: `{"updated_at":1620767605}`,
want: dst{UpdatedAt: 1620767605},
},
{
name: "time parse error",
json: `{"updated_at":"foo"}`,
wantErr: true,
},
{
name: "null",
json: `{"updated_at":null}`,
},
{
name: "invalid type",
json: `{"updated_at":["foo","bar"]}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got dst
err := json.Unmarshal([]byte(tt.json), &got)
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want, got)
})
}
t.Run("syntax error", func(t *testing.T) {
var ts Time
err := ts.UnmarshalJSON([]byte{'~'})
assert.Error(t, err)
})
}

View file

@ -1,78 +1,320 @@
package oidc package oidc
// UserInfo implements OpenID Connect Core 1.0, section 5.1. import (
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims. "encoding/json"
type UserInfo struct { "fmt"
Subject string `json:"sub,omitempty"` "time"
"golang.org/x/text/language"
)
type UserInfo interface {
GetSubject() string
UserInfoProfile UserInfoProfile
UserInfoEmail UserInfoEmail
UserInfoPhone UserInfoPhone
Address *UserInfoAddress `json:"address,omitempty"` GetAddress() UserInfoAddress
GetClaim(key string) interface{}
Claims map[string]any `json:"-"` GetClaims() map[string]interface{}
} }
func (u *UserInfo) AppendClaims(k string, v any) { type UserInfoProfile interface {
if u.Claims == nil { GetName() string
u.Claims = make(map[string]any) GetGivenName() string
} GetFamilyName() string
GetMiddleName() string
u.Claims[k] = v GetNickname() string
GetProfile() string
GetPicture() string
GetWebsite() string
GetGender() Gender
GetBirthdate() string
GetZoneinfo() string
GetLocale() language.Tag
GetPreferredUsername() string
} }
// GetAddress is a safe getter that takes type UserInfoEmail interface {
// care of a possible nil value. GetEmail() string
func (u *UserInfo) GetAddress() *UserInfoAddress { IsEmailVerified() bool
}
type UserInfoPhone interface {
GetPhoneNumber() string
IsPhoneNumberVerified() bool
}
type UserInfoAddress interface {
GetFormatted() string
GetStreetAddress() string
GetLocality() string
GetRegion() string
GetPostalCode() string
GetCountry() string
}
type UserInfoSetter interface {
UserInfo
SetSubject(sub string)
UserInfoProfileSetter
SetEmail(email string, verified bool)
SetPhone(phone string, verified bool)
SetAddress(address UserInfoAddress)
AppendClaims(key string, values interface{})
}
type UserInfoProfileSetter interface {
SetName(name string)
SetGivenName(name string)
SetFamilyName(name string)
SetMiddleName(name string)
SetNickname(name string)
SetUpdatedAt(date time.Time)
SetProfile(profile string)
SetPicture(profile string)
SetWebsite(website string)
SetGender(gender Gender)
SetBirthdate(birthdate string)
SetZoneinfo(zoneInfo string)
SetLocale(locale language.Tag)
SetPreferredUsername(name string)
}
func NewUserInfo() UserInfoSetter {
return &userinfo{}
}
type userinfo struct {
Subject string `json:"sub,omitempty"`
userInfoProfile
userInfoEmail
userInfoPhone
Address UserInfoAddress `json:"address,omitempty"`
claims map[string]interface{}
}
func (u *userinfo) GetSubject() string {
return u.Subject
}
func (u *userinfo) GetName() string {
return u.Name
}
func (u *userinfo) GetGivenName() string {
return u.GivenName
}
func (u *userinfo) GetFamilyName() string {
return u.FamilyName
}
func (u *userinfo) GetMiddleName() string {
return u.MiddleName
}
func (u *userinfo) GetNickname() string {
return u.Nickname
}
func (u *userinfo) GetProfile() string {
return u.Profile
}
func (u *userinfo) GetPicture() string {
return u.Picture
}
func (u *userinfo) GetWebsite() string {
return u.Website
}
func (u *userinfo) GetGender() Gender {
return u.Gender
}
func (u *userinfo) GetBirthdate() string {
return u.Birthdate
}
func (u *userinfo) GetZoneinfo() string {
return u.Zoneinfo
}
func (u *userinfo) GetLocale() language.Tag {
return u.Locale
}
func (u *userinfo) GetPreferredUsername() string {
return u.PreferredUsername
}
func (u *userinfo) GetEmail() string {
return u.Email
}
func (u *userinfo) IsEmailVerified() bool {
return bool(u.EmailVerified)
}
func (u *userinfo) GetPhoneNumber() string {
return u.PhoneNumber
}
func (u *userinfo) IsPhoneNumberVerified() bool {
return u.PhoneNumberVerified
}
func (u *userinfo) GetAddress() UserInfoAddress {
if u.Address == nil { if u.Address == nil {
return new(UserInfoAddress) return &userInfoAddress{}
} }
return u.Address return u.Address
} }
// GetSubject implements [rp.SubjectGetter] func (u *userinfo) GetClaim(key string) interface{} {
func (u *UserInfo) GetSubject() string { return u.claims[key]
return u.Subject
} }
type uiAlias UserInfo func (u *userinfo) GetClaims() map[string]interface{} {
return u.claims
func (u *UserInfo) MarshalJSON() ([]byte, error) {
return mergeAndMarshalClaims((*uiAlias)(u), u.Claims)
} }
func (u *UserInfo) UnmarshalJSON(data []byte) error { func (u *userinfo) SetSubject(sub string) {
return unmarshalJSONMulti(data, (*uiAlias)(u), &u.Claims) u.Subject = sub
} }
type UserInfoProfile struct { func (u *userinfo) SetName(name string) {
Name string `json:"name,omitempty"` u.Name = name
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
Nickname string `json:"nickname,omitempty"`
Profile string `json:"profile,omitempty"`
Picture string `json:"picture,omitempty"`
Website string `json:"website,omitempty"`
Gender Gender `json:"gender,omitempty"`
Birthdate string `json:"birthdate,omitempty"`
Zoneinfo string `json:"zoneinfo,omitempty"`
Locale *Locale `json:"locale,omitempty"`
UpdatedAt Time `json:"updated_at,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
} }
type UserInfoEmail struct { func (u *userinfo) SetGivenName(name string) {
u.GivenName = name
}
func (u *userinfo) SetFamilyName(name string) {
u.FamilyName = name
}
func (u *userinfo) SetMiddleName(name string) {
u.MiddleName = name
}
func (u *userinfo) SetNickname(name string) {
u.Nickname = name
}
func (u *userinfo) SetUpdatedAt(date time.Time) {
u.UpdatedAt = Time(date)
}
func (u *userinfo) SetProfile(profile string) {
u.Profile = profile
}
func (u *userinfo) SetPicture(picture string) {
u.Picture = picture
}
func (u *userinfo) SetWebsite(website string) {
u.Website = website
}
func (u *userinfo) SetGender(gender Gender) {
u.Gender = gender
}
func (u *userinfo) SetBirthdate(birthdate string) {
u.Birthdate = birthdate
}
func (u *userinfo) SetZoneinfo(zoneInfo string) {
u.Zoneinfo = zoneInfo
}
func (u *userinfo) SetLocale(locale language.Tag) {
u.Locale = locale
}
func (u *userinfo) SetPreferredUsername(name string) {
u.PreferredUsername = name
}
func (u *userinfo) SetEmail(email string, verified bool) {
u.Email = email
u.EmailVerified = boolString(verified)
}
func (u *userinfo) SetPhone(phone string, verified bool) {
u.PhoneNumber = phone
u.PhoneNumberVerified = verified
}
func (u *userinfo) SetAddress(address UserInfoAddress) {
u.Address = address
}
func (u *userinfo) AppendClaims(key string, value interface{}) {
if u.claims == nil {
u.claims = make(map[string]interface{})
}
u.claims[key] = value
}
func (u *userInfoAddress) GetFormatted() string {
return u.Formatted
}
func (u *userInfoAddress) GetStreetAddress() string {
return u.StreetAddress
}
func (u *userInfoAddress) GetLocality() string {
return u.Locality
}
func (u *userInfoAddress) GetRegion() string {
return u.Region
}
func (u *userInfoAddress) GetPostalCode() string {
return u.PostalCode
}
func (u *userInfoAddress) GetCountry() string {
return u.Country
}
type userInfoProfile struct {
Name string `json:"name,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
Nickname string `json:"nickname,omitempty"`
Profile string `json:"profile,omitempty"`
Picture string `json:"picture,omitempty"`
Website string `json:"website,omitempty"`
Gender Gender `json:"gender,omitempty"`
Birthdate string `json:"birthdate,omitempty"`
Zoneinfo string `json:"zoneinfo,omitempty"`
Locale language.Tag `json:"locale,omitempty"`
UpdatedAt Time `json:"updated_at,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
}
type userInfoEmail struct {
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
// Handle providers that return email_verified as a string // Handle providers that return email_verified as a string
// https://forums.aws.amazon.com/thread.jspa?messageID=949441&#949441 // https://forums.aws.amazon.com/thread.jspa?messageID=949441&#949441
// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11 // https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
EmailVerified Bool `json:"email_verified,omitempty"` EmailVerified boolString `json:"email_verified,omitempty"`
} }
type Bool bool type boolString bool
func (bs *Bool) UnmarshalJSON(data []byte) error { func (bs *boolString) UnmarshalJSON(data []byte) error {
if string(data) == "true" || string(data) == `"true"` { if string(data) == "true" || string(data) == `"true"` {
*bs = true *bs = true
} }
@ -80,12 +322,12 @@ func (bs *Bool) UnmarshalJSON(data []byte) error {
return nil return nil
} }
type UserInfoPhone struct { type userInfoPhone struct {
PhoneNumber string `json:"phone_number,omitempty"` PhoneNumber string `json:"phone_number,omitempty"`
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"` PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
} }
type UserInfoAddress struct { type userInfoAddress struct {
Formatted string `json:"formatted,omitempty"` Formatted string `json:"formatted,omitempty"`
StreetAddress string `json:"street_address,omitempty"` StreetAddress string `json:"street_address,omitempty"`
Locality string `json:"locality,omitempty"` Locality string `json:"locality,omitempty"`
@ -94,6 +336,76 @@ type UserInfoAddress struct {
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
} }
func NewUserInfoAddress(streetAddress, locality, region, postalCode, country, formatted string) UserInfoAddress {
return &userInfoAddress{
StreetAddress: streetAddress,
Locality: locality,
Region: region,
PostalCode: postalCode,
Country: country,
Formatted: formatted,
}
}
func (u *userinfo) MarshalJSON() ([]byte, error) {
type Alias userinfo
a := &struct {
*Alias
Locale interface{} `json:"locale,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}{
Alias: (*Alias)(u),
}
if !u.Locale.IsRoot() {
a.Locale = u.Locale
}
if !time.Time(u.UpdatedAt).IsZero() {
a.UpdatedAt = time.Time(u.UpdatedAt).Unix()
}
b, err := json.Marshal(a)
if err != nil {
return nil, err
}
if len(u.claims) == 0 {
return b, nil
}
err = json.Unmarshal(b, &u.claims)
if err != nil {
return nil, fmt.Errorf("jws: invalid map of custom claims %v", u.claims)
}
return json.Marshal(u.claims)
}
func (u *userinfo) UnmarshalJSON(data []byte) error {
type Alias userinfo
a := &struct {
Address *userInfoAddress `json:"address,omitempty"`
*Alias
UpdatedAt int64 `json:"update_at,omitempty"`
}{
Alias: (*Alias)(u),
}
if err := json.Unmarshal(data, &a); err != nil {
return err
}
if a.Address != nil {
u.Address = a.Address
}
u.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC())
if err := json.Unmarshal(data, &u.claims); err != nil {
return err
}
return nil
}
type UserInfoRequest struct { type UserInfoRequest struct {
AccessToken string `schema:"access_token"` AccessToken string `schema:"access_token"`
} }

Some files were not shown because too many files have changed in this diff Show more