pax_global_header 0000666 0000000 0000000 00000000064 14656014552 0014522 g ustar 00root root 0000000 0000000 52 comment=f340574bc090fc4c47e81be34445524459aecc44
golang-github-zitadel-oidc-3.27.0/ 0000775 0000000 0000000 00000000000 14656014552 0016710 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/.codecov/ 0000775 0000000 0000000 00000000000 14656014552 0020410 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/.codecov/codecov.yml 0000664 0000000 0000000 00000000726 14656014552 0022562 0 ustar 00root root 0000000 0000000 codecov:
branch: main
notify:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project: yes
patch: yes
changes: no
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "header, diff"
behavior: default
require_changes: no
ignore:
- "example"
- "**/mock"
golang-github-zitadel-oidc-3.27.0/.github/ 0000775 0000000 0000000 00000000000 14656014552 0020250 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14656014552 0022433 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/.github/ISSUE_TEMPLATE/bug_report.yaml 0000664 0000000 0000000 00000003470 14656014552 0025473 0 ustar 00root root 0000000 0000000 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"]
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.
golang-github-zitadel-oidc-3.27.0/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000032 14656014552 0024416 0 ustar 00root root 0000000 0000000 blank_issues_enabled: true golang-github-zitadel-oidc-3.27.0/.github/ISSUE_TEMPLATE/docs.yaml 0000664 0000000 0000000 00000001627 14656014552 0024255 0 ustar 00root root 0000000 0000000 name: đ Documentation
description: Create an issue for missing or wrong documentation.
labels: ["docs"]
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.
golang-github-zitadel-oidc-3.27.0/.github/ISSUE_TEMPLATE/improvement.yaml 0000664 0000000 0000000 00000003060 14656014552 0025663 0 ustar 00root root 0000000 0000000 name: đ ī¸ Improvement
description: "Create an new issue for an improvment in ZITADEL"
labels: ["improvement"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this improvement request
- 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.
golang-github-zitadel-oidc-3.27.0/.github/ISSUE_TEMPLATE/proposal.yaml 0000664 0000000 0000000 00000002560 14656014552 0025161 0 ustar 00root root 0000000 0000000 name: đĄ Proposal / Feature request
description: "Create an issue for a feature request/proposal."
labels: ["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 proposal / feature 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: textarea
id: additional
attributes:
label: Additional Context
description: Please add any other infos that could be useful.
golang-github-zitadel-oidc-3.27.0/.github/dependabot.yml 0000664 0000000 0000000 00000000755 14656014552 0023107 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
time: '04:00'
open-pull-requests-limit: 10
commit-message:
prefix: chore
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"
directory: "/"
schedule:
interval: weekly golang-github-zitadel-oidc-3.27.0/.github/pull_request_template.md 0000664 0000000 0000000 00000001322 14656014552 0025207 0 ustar 00root root 0000000 0000000 ### 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.
golang-github-zitadel-oidc-3.27.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14656014552 0022305 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/.github/workflows/codeql-analysis.yml 0000664 0000000 0000000 00000003152 14656014552 0026121 0 ustar 00root root 0000000 0000000 name: "Code scanning - action"
on:
push:
branches: [main,next]
pull_request:
# The branches below must be a subset of the branches above
branches: [main,next]
schedule:
- cron: '0 11 * * 0'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
# Override language selection by uncommenting this and choosing your languages
with:
languages: go
# 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)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# âšī¸ Command-line programs to run using the OS shell.
# đ https://git.io/JvXDl
# âī¸ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
golang-github-zitadel-oidc-3.27.0/.github/workflows/issue.yml 0000664 0000000 0000000 00000003134 14656014552 0024161 0 ustar 00root root 0000000 0000000 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
golang-github-zitadel-oidc-3.27.0/.github/workflows/release.yml 0000664 0000000 0000000 00000002267 14656014552 0024457 0 ustar 00root root 0000000 0000000 name: Release
on:
push:
branches:
- "2.11.x"
- main
- next
tags-ignore:
- '**'
pull_request:
branches:
- '**'
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
go: ['1.21', '1.22']
name: Go ${{ matrix.go }} test
steps:
- uses: actions/checkout@v4
- name: Setup go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/...
- uses: codecov/codecov-action@v4.5.0
with:
file: ./profile.cov
name: codecov-go
release:
runs-on: ubuntu-20.04
needs: [test]
if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Source checkout
uses: actions/checkout@v4
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v4
with:
dry_run: false
semantic_version: 18.0.1
extra_plugins: |
@semantic-release/exec@6.0.3
golang-github-zitadel-oidc-3.27.0/.gitignore 0000664 0000000 0000000 00000000350 14656014552 0020676 0 ustar 00root root 0000000 0000000 # Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
**/__debug_bin
.vscode
.DS_Store
.idea
golang-github-zitadel-oidc-3.27.0/.releaserc.js 0000664 0000000 0000000 00000000443 14656014552 0021272 0 ustar 00root root 0000000 0000000 module.exports = {
branches: [
{name: "2.11.x"},
{name: "main"},
{name: "next", prerelease: true},
],
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
};
golang-github-zitadel-oidc-3.27.0/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000012142 14656014552 0021507 0 ustar 00root root 0000000 0000000 # Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
abuse@zitadel.ch.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
golang-github-zitadel-oidc-3.27.0/CONTRIBUTING.md 0000664 0000000 0000000 00000002603 14656014552 0021142 0 ustar 00root root 0000000 0000000 # How to contribute to the OIDC SDK for Go
## Did you find a bug?
Please file an issue [here](https://github.com/zitadel/oidc/issues/new?assignees=&labels=bug&template=bug_report.md&title=).
Bugs are evaluated every day as soon as possible.
## Enhancement
Do you miss a feature? Please file an issue [here](https://github.com/zitadel/oidc/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)
Enhancements are discussed and evaluated every Wednesday by the ZITADEL core team.
## Grab an Issues
We add the label "good first issue" for problems we think are a good starting point to contribute to the OIDC SDK.
* [Issues for first time contributors](https://github.com/zitadel/oidc/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
* [All issues](https://github.com/zitadel/oidc/issues)
### Make a PR
If you like to contribute fork the OIDC repository. After you implemented the new feature create a PullRequest in the OIDC reposiotry.
Make sure you use semantic release:
* feat: New Feature
* fix: Bug Fix
* docs: Documentation
## Want to use the library?
Checkout the [examples folder](example) for different client and server implementations.
Or checkout how we use it ourselves in our OpenSource Identity and Access Management [ZITADEL](https://github.com/zitadel/zitadel).
## **Did you find a security flaw?**
* Please read [Security Policy](SECURITY.md). golang-github-zitadel-oidc-3.27.0/LICENSE 0000664 0000000 0000000 00000026135 14656014552 0017724 0 ustar 00root root 0000000 0000000 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "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.
golang-github-zitadel-oidc-3.27.0/NOTICE 0000664 0000000 0000000 00000000050 14656014552 0017607 0 ustar 00root root 0000000 0000000 Copyright The zitadel/oidc Contributors
golang-github-zitadel-oidc-3.27.0/README.md 0000664 0000000 0000000 00000021564 14656014552 0020177 0 ustar 00root root 0000000 0000000 # OpenID Connect SDK (client and server) for Go
[](https://github.com/semantic-release/semantic-release)
[](https://github.com/zitadel/oidc/actions)
[](https://pkg.go.dev/github.com/zitadel/oidc/v3)
[](https://github.com/zitadel/oidc/blob/master/LICENSE)
[](https://github.com/zitadel/oidc/releases)
[](https://goreportcard.com/report/github.com/zitadel/oidc/v3)
[](https://codecov.io/gh/zitadel/oidc)
[](https://openid.net/certification/)
## What Is It
This project is an easy-to-use client (RP) and server (OP) implementation for the `OIDC` (OpenID Connect) standard written for `Go`.
The RP is certified for the [basic](https://www.certification.openid.net/plan-detail.html?public=true&plan=uoprP0OO8Z4Qo) and [config](https://www.certification.openid.net/plan-detail.html?public=true&plan=AYSdLbzmWbu9X) profile.
Whenever possible we tried to reuse / extend existing packages like `OAuth2 for Go`.
## Basic Overview
The most important packages of the library:
/pkg
/client clients using the OP for retrieving, exchanging and verifying tokens
/rp definition and implementation of an OIDC Relying Party (client)
/rs definition and implementation of an OAuth Resource Server (API)
/op definition and implementation of an OIDC OpenID Provider (server)
/oidc definitions shared by clients and server
/example
/client/api example of an api / resource server implementation using token introspection
/client/app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
/client/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
/client/service demonstration of JWT Profile Authorization Grant
/server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI
### 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
Check the `/example` folder where example code for different scenarios is located.
```bash
# start oidc op server
# oidc discovery http://localhost:9998/.well-known/openid-configuration
go run github.com/zitadel/oidc/v3/example/server
# 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
```
- open http://localhost:9999/login in your browser
- you will be redirected to op server and the login UI
- login with user `test-user@localhost` and password `verysecure`
- the OP will redirect you to the client app, which displays the user info
for the dynamic issuer, just start it with:
```bash
go run github.com/zitadel/oidc/v3/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:
```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
```
> Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`)
## Features
| | Relying party | OpenID Provider | Specification |
| -------------------- | ------------- | --------------- | ----------------------------------------- |
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] |
| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] |
| 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] |
[1]: "3.1. Authentication using the Authorization Code Flow"
[2]: "3.2. Authentication using the Implicit Flow"
[3]: "3.3. Authentication using the Hybrid Flow"
[4]: "9. Client Authentication"
[5]: "12. Using Refresh Tokens"
[6]: "OpenID Connect Discovery 1.0 incorporating errata set 1"
[7]: "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
[8]: "Proof Key for Code Exchange by OAuth Public Clients"
[9]: "OAuth 2.0 Token Exchange"
[10]: "OAuth 2.0 Device Authorization Grant"
[11]: "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
## Contributors
Made with [contrib.rocks](https://contrib.rocks).
### Resources
For your convenience you can find the relevant guides linked below.
- [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)
## Supported Go Versions
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:.
| Version | Supported |
| ------- | ------------------ |
| <1.21 | :x: |
| 1.21 | :white_check_mark: |
| 1.22 | :white_check_mark: |
## Why another library
As of 2020 there are not a lot of `OIDC` library's in `Go` which can handle server and client implementations. ZITADEL is strongly committed to the general field of IAM (Identity and Access Management) and as such, we need solid frameworks to implement services.
### Goals
- [Certify this library as OP](https://openid.net/certification/#OPs)
### Other Go OpenID Connect libraries
[https://github.com/coreos/go-oidc](https://github.com/coreos/go-oidc)
The `go-oidc` does only support `RP` and is not feasible to use as `OP` that's why we could not rely on `go-oidc`
[https://github.com/ory/fosite](https://github.com/ory/fosite)
We did not choose `fosite` because it implements `OAuth 2.0` on its own and does not rely on the golang provided package. Nonetheless this is a great project.
## License
The full functionality of this library is and stays open source and free to use for everyone. Visit
our [website](https://zitadel.com) and get in touch.
See the exact licensing terms [here](LICENSE)
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "
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.
[^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892
golang-github-zitadel-oidc-3.27.0/SECURITY.md 0000664 0000000 0000000 00000002121 14656014552 0020475 0 ustar 00root root 0000000 0000000 # 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.
## Supported Versions
We currently support the following version of the OIDC framework:
| Version | Supported | Branch | Details |
| -------- | ------------------ | ----------- | ------------------------------------ |
| 0.x.x | :x: | | not maintained |
| <2.11 | :x: | | not maintained |
| 2.11.x | :lock: :warning: | [2.11.x][1] | security only, [community effort][2] |
| 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
[2]: https://github.com/zitadel/oidc/discussions/458
[3]: https://github.com/zitadel/oidc/tree/main
[4]: https://github.com/zitadel/oidc/tree/next
golang-github-zitadel-oidc-3.27.0/UPGRADING.md 0000664 0000000 0000000 00000044206 14656014552 0020560 0 ustar 00root root 0000000 0000000 # 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
``` golang-github-zitadel-oidc-3.27.0/doc.go 0000664 0000000 0000000 00000000015 14656014552 0020000 0 ustar 00root root 0000000 0000000 package oidc
golang-github-zitadel-oidc-3.27.0/example/ 0000775 0000000 0000000 00000000000 14656014552 0020343 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/client/ 0000775 0000000 0000000 00000000000 14656014552 0021621 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/client/api/ 0000775 0000000 0000000 00000000000 14656014552 0022372 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/client/api/api.go 0000664 0000000 0000000 00000005501 14656014552 0023473 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/sirupsen/logrus"
"github.com/zitadel/oidc/v3/pkg/client/rs"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
const (
publicURL string = "/public"
protectedURL string = "/protected"
protectedClaimURL string = "/protected/{claim}/{value}"
)
func main() {
keyPath := os.Getenv("KEY")
port := os.Getenv("PORT")
issuer := os.Getenv("ISSUER")
provider, err := rs.NewResourceServerFromKeyFile(context.TODO(), issuer, keyPath)
if err != nil {
logrus.Fatalf("error creating provider %s", err.Error())
}
router := chi.NewRouter()
// public url accessible without any authorization
// will print `OK` and current timestamp
router.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK " + time.Now().String()))
})
// protected url which needs an active token
// will print the result of the introspection endpoint on success
router.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) {
ok, token := checkToken(w, r)
if !ok {
return
}
resp, err := rs.Introspect[*oidc.IntrospectionResponse](r.Context(), provider, token)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
data, err := json.Marshal(resp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(data)
})
// protected url which needs an active token and checks if the response of the introspect endpoint
// contains a requested claim with the required (string) value
// e.g. /protected/username/livio@zitadel.example
router.HandleFunc(protectedClaimURL, func(w http.ResponseWriter, r *http.Request) {
ok, token := checkToken(w, r)
if !ok {
return
}
resp, err := rs.Introspect[*oidc.IntrospectionResponse](r.Context(), provider, token)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
requestedClaim := chi.URLParam(r, "claim")
requestedValue := chi.URLParam(r, "value")
value, ok := resp.Claims[requestedClaim].(string)
if !ok || value == "" || value != requestedValue {
http.Error(w, "claim does not match", http.StatusForbidden)
return
}
w.Write([]byte("authorized with value " + value))
})
lis := fmt.Sprintf("127.0.0.1:%s", port)
log.Printf("listening on http://%s/", lis)
log.Fatal(http.ListenAndServe(lis, router))
}
func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) {
auth := r.Header.Get("authorization")
if auth == "" {
http.Error(w, "auth header missing", http.StatusUnauthorized)
return false, ""
}
if !strings.HasPrefix(auth, oidc.PrefixBearer) {
http.Error(w, "invalid header", http.StatusUnauthorized)
return false, ""
}
return true, strings.TrimPrefix(auth, oidc.PrefixBearer)
}
golang-github-zitadel-oidc-3.27.0/example/client/app/ 0000775 0000000 0000000 00000000000 14656014552 0022401 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/client/app/app.go 0000664 0000000 0000000 00000013473 14656014552 0023520 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"strings"
"sync/atomic"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
var (
callbackPath = "/auth/callback"
key = []byte("test1234test1234")
)
func main() {
clientID := os.Getenv("CLIENT_ID")
clientSecret := os.Getenv("CLIENT_SECRET")
keyPath := os.Getenv("KEY_PATH")
issuer := os.Getenv("ISSUER")
port := os.Getenv("PORT")
scopes := strings.Split(os.Getenv("SCOPES"), " ")
responseMode := os.Getenv("RESPONSE_MODE")
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
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{
rp.WithCookieHandler(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
rp.WithHTTPClient(client),
rp.WithLogger(logger),
}
if clientSecret == "" {
options = append(options, rp.WithPKCE(cookieHandler))
}
if keyPath != "" {
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
}
// One can add a logger to the context,
// 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 {
logrus.Fatalf("error creating provider %s", err.Error())
}
// generate some state (representing the state of the user in your application,
// e.g. the page where he was before sending him to login
state := func() string {
return uuid.New().String()
}
urlOptions := []rp.URLParamOpt{
rp.WithPromptURLParam("Welcome back!"),
}
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
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], 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)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("content-type", "application/json")
w.Write(data)
}
// you could also just take the access_token and id_token without calling the userinfo endpoint:
//
// marshalToken := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) {
// data, err := json.Marshal(tokens)
// if err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// return
// }
// w.Write(data)
//}
// you can also try token exchange flow
//
// requestTokenExchange := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
// data := make(url.Values)
// data.Set("grant_type", string(oidc.GrantTypeTokenExchange))
// data.Set("requested_token_type", string(oidc.IDTokenType))
// data.Set("subject_token", tokens.RefreshToken)
// data.Set("subject_token_type", string(oidc.RefreshTokenType))
// data.Add("scope", "profile custom_scope:impersonate:id2")
// client := &http.Client{}
// r2, _ := http.NewRequest(http.MethodPost, issuer+"/oauth/token", strings.NewReader(data.Encode()))
// // r2.Header.Add("Authorization", "Basic "+"d2ViOnNlY3JldA==")
// r2.Header.Add("Content-Type", "application/x-www-form-urlencoded")
// r2.SetBasicAuth("web", "secret")
// resp, _ := client.Do(r2)
// fmt.Println(resp.Status)
// b, _ := io.ReadAll(resp.Body)
// resp.Body.Close()
// w.Write(b)
// }
// register the CodeExchangeHandler at the callbackPath
// the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
// with the returned tokens from the token endpoint
// in this example the callback function itself is wrapped by the UserinfoCallback which
// will call the Userinfo endpoint, check the sub and pass the info into the callback function
http.Handle(callbackPath, rp.CodeExchangeHandler(rp.UserinfoCallback(marshalUserinfo), provider))
// if you would use the callback without calling the userinfo endpoint, simply switch the callback handler for:
//
// 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)
logger.Info("server listening, press ctrl+c to stop", "addr", lis)
err = http.ListenAndServe(lis, mw(http.DefaultServeMux))
if err != http.ErrServerClosed {
logger.Error("server terminated", "error", err)
os.Exit(1)
}
}
golang-github-zitadel-oidc-3.27.0/example/client/device/ 0000775 0000000 0000000 00000000000 14656014552 0023060 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/client/device/device.go 0000664 0000000 0000000 00000005513 14656014552 0024652 0 ustar 00root root 0000000 0000000 // 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
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/sirupsen/logrus"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
)
var (
key = []byte("test1234test1234")
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT)
defer stop()
clientID := os.Getenv("CLIENT_ID")
clientSecret := os.Getenv("CLIENT_SECRET")
keyPath := os.Getenv("KEY_PATH")
issuer := os.Getenv("ISSUER")
scopes := strings.Split(os.Getenv("SCOPES"), " ")
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
var options []rp.Option
if clientSecret == "" {
options = append(options, rp.WithPKCE(cookieHandler))
}
if keyPath != "" {
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
}
provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, "", scopes, options...)
if err != nil {
logrus.Fatalf("error creating provider %s", err.Error())
}
logrus.Info("starting device authorization flow")
resp, err := rp.DeviceAuthorization(ctx, scopes, provider, nil)
if err != nil {
logrus.Fatal(err)
}
logrus.Info("resp", resp)
fmt.Printf("\nPlease browse to %s and enter code %s\n", resp.VerificationURI, resp.UserCode)
logrus.Info("start polling")
token, err := rp.DeviceAccessToken(ctx, resp.DeviceCode, time.Duration(resp.Interval)*time.Second, provider)
if err != nil {
logrus.Fatal(err)
}
logrus.Infof("successfully obtained token: %#v", token)
}
golang-github-zitadel-oidc-3.27.0/example/client/github/ 0000775 0000000 0000000 00000000000 14656014552 0023103 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/client/github/github.go 0000664 0000000 0000000 00000002670 14656014552 0024721 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"fmt"
"os"
"github.com/google/go-github/v31/github"
"github.com/google/uuid"
"golang.org/x/oauth2"
githubOAuth "golang.org/x/oauth2/github"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/client/rp/cli"
"github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
var (
callbackPath = "/orbctl/github/callback"
key = []byte("test1234test1234")
)
func main() {
clientID := os.Getenv("CLIENT_ID")
clientSecret := os.Getenv("CLIENT_SECRET")
port := os.Getenv("PORT")
rpConfig := &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: fmt.Sprintf("http://localhost:%v%v", port, callbackPath),
Scopes: []string{"repo", "repo_deployment"},
Endpoint: githubOAuth.Endpoint,
}
ctx := context.Background()
cookieHandler := http.NewCookieHandler(key, key, http.WithUnsecure())
relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler))
if err != nil {
fmt.Printf("error creating relaying party: %v", err)
return
}
state := func() string {
return uuid.New().String()
}
token := cli.CodeFlow[*oidc.IDTokenClaims](ctx, relyingParty, callbackPath, port, state)
client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token))
_, _, err = client.Users.Get(ctx, "")
if err != nil {
fmt.Printf("error %v", err)
return
}
fmt.Println("call succeeded")
}
golang-github-zitadel-oidc-3.27.0/example/client/service/ 0000775 0000000 0000000 00000000000 14656014552 0023261 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/client/service/service.go 0000664 0000000 0000000 00000010152 14656014552 0025247 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"os"
"strings"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/pkg/client/profile"
)
var client = http.DefaultClient
func main() {
keyPath := os.Getenv("KEY_PATH")
issuer := os.Getenv("ISSUER")
port := os.Getenv("PORT")
scopes := strings.Split(os.Getenv("SCOPES"), " ")
if keyPath != "" {
ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(context.TODO(), issuer, keyPath, scopes)
if err != nil {
logrus.Fatalf("error creating token source %s", err.Error())
}
client = oauth2.NewClient(context.Background(), ts)
}
http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
tpl := `
Login
`
t, err := template.New("login").Parse(tpl)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
err := r.ParseMultipartForm(4 << 10)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
file, _, err := r.FormFile("key")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
key, err := io.ReadAll(file)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(context.TODO(), issuer, key, scopes)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
client = oauth2.NewClient(context.Background(), ts)
token, err := ts.Token()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data, err := json.Marshal(token)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(data)
}
})
http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
tpl := `
Test
{{if .URL}}
Result for {{.URL}}: {{.Response}}
{{end}}
`
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
testURL := r.Form.Get("url")
var data struct {
URL string
Response any
}
if testURL != "" {
data.URL = testURL
data.Response, err = callExampleEndpoint(client, testURL)
if err != nil {
data.Response = err
}
}
t, err := template.New("login").Parse(tpl)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
lis := fmt.Sprintf("127.0.0.1:%s", port)
logrus.Infof("listening on http://%s/", lis)
logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil))
}
func callExampleEndpoint(client *http.Client, testURL string) (any, error) {
req, err := http.NewRequest("GET", testURL, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("http status not ok: %s %s", resp.Status, body)
}
if strings.HasPrefix(resp.Header.Get("content-type"), "text/plain") {
return string(body), nil
}
return body, err
}
golang-github-zitadel-oidc-3.27.0/example/doc.go 0000664 0000000 0000000 00000001075 14656014552 0021442 0 ustar 00root root 0000000 0000000 /*
Package example contains some example of the various use of this library:
/api example of an api / resource server implementation using token introspection
/app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
/service demonstration of JWT Profile Authorization Grant
/server examples of an OpenID Provider implementations (including dynamic) with some very basic
*/
package example
golang-github-zitadel-oidc-3.27.0/example/server/ 0000775 0000000 0000000 00000000000 14656014552 0021651 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/server/dynamic/ 0000775 0000000 0000000 00000000000 14656014552 0023275 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/server/dynamic/login.go 0000664 0000000 0000000 00000005634 14656014552 0024744 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"fmt"
"html/template"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/zitadel/oidc/v3/pkg/op"
)
const (
queryAuthRequestID = "authRequestID"
)
var (
loginTmpl, _ = template.New("login").Parse(`
Login
`)
)
type login struct {
authenticate authenticate
router chi.Router
callback func(context.Context, string) string
}
func NewLogin(authenticate authenticate, callback func(context.Context, string) string, issuerInterceptor *op.IssuerInterceptor) *login {
l := &login{
authenticate: authenticate,
callback: callback,
}
l.createRouter(issuerInterceptor)
return l
}
func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) {
l.router = chi.NewRouter()
l.router.Get("/username", l.loginHandler)
l.router.With(issuerInterceptor.Handler).Post("/username", l.checkLoginHandler)
}
type authenticate interface {
CheckUsernamePassword(ctx context.Context, username, password, id string) error
}
func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError)
return
}
//the oidc package will pass the id of the auth request as query parameter
//we will use this id through the login process and therefore pass it to the login page
renderLogin(w, r.FormValue(queryAuthRequestID), nil)
}
func renderLogin(w http.ResponseWriter, id string, err error) {
var errMsg string
if err != nil {
errMsg = err.Error()
}
data := &struct {
ID string
Error string
}{
ID: id,
Error: errMsg,
}
err = loginTmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
id := r.FormValue("id")
err = l.authenticate.CheckUsernamePassword(r.Context(), username, password, id)
if err != nil {
renderLogin(w, id, err)
return
}
http.Redirect(w, r, l.callback(r.Context(), id), http.StatusFound)
}
golang-github-zitadel-oidc-3.27.0/example/server/dynamic/op.go 0000664 0000000 0000000 00000010772 14656014552 0024251 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"crypto/sha256"
"fmt"
"log"
"net/http"
"github.com/go-chi/chi/v5"
"golang.org/x/text/language"
"github.com/zitadel/oidc/v3/example/server/storage"
"github.com/zitadel/oidc/v3/pkg/op"
)
const (
pathLoggedOut = "/logged-out"
)
var (
hostnames = []string{
"localhost", //note that calling 127.0.0.1 / ::1 won't work as the hostname does not match
"oidc.local", //add this to your hosts file (pointing to 127.0.0.1)
//feel free to add more...
}
)
func init() {
storage.RegisterClients(
storage.NativeClient("native"),
storage.WebClient("web", "secret"),
storage.WebClient("api", "secret"),
)
}
func main() {
ctx := context.Background()
port := "9998"
issuers := make([]string, len(hostnames))
for i, hostname := range hostnames {
issuers[i] = fmt.Sprintf("http://%s:%s/", hostname, port)
}
//the OpenID Provider requires a 32-byte key for (token) encryption
//be sure to create a proper crypto random key and manage it securely!
key := sha256.Sum256([]byte("test"))
router := chi.NewRouter()
//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) {
_, err := w.Write([]byte("signed out successfully"))
if err != nil {
log.Printf("error serving logged out page: %v", err)
}
})
//the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
//this might be the layer for accessing your database
//in this example it will be handled in-memory
//the NewMultiStorage is able to handle multiple issuers
storage := storage.NewMultiStorage(issuers)
//creation of the OpenIDProvider with the just created in-memory Storage
provider, err := newDynamicOP(ctx, storage, key)
if err != nil {
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
//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), op.NewIssuerInterceptor(provider.IssuerFromRequest))
//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
router.Mount("/login/", 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)
//is served on the correct 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/):
//router.PathPrefix("/custom/path/").Handler(http.StripPrefix("/custom/path", provider.HttpHandler()))
router.Mount("/", provider)
server := &http.Server{
Addr: ":" + port,
Handler: router,
}
err = server.ListenAndServe()
if err != nil {
log.Fatal(err)
}
<-ctx.Done()
}
// newDynamicOP will create an OpenID Provider for localhost on a specified port with a given encryption key
// and a predefined default logout uri
// it will enable all options (see descriptions)
func newDynamicOP(ctx context.Context, storage op.Storage, key [32]byte) (*op.Provider, error) {
config := &op.Config{
CryptoKey: key,
//will be used if the end_session endpoint is called without a post_logout_redirect_uri
DefaultLogoutRedirectURI: pathLoggedOut,
//enables code_challenge_method S256 for PKCE (and therefore PKCE in general)
CodeMethodS256: true,
//enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth)
AuthMethodPost: true,
//enables additional authentication by using private_key_jwt
AuthMethodPrivateKeyJWT: true,
//enables refresh_token grant use
GrantTypeRefreshToken: true,
//enables use of the `request` Object parameter
RequestObjectSupported: true,
//this example has only static texts (in English), so we'll set the here accordingly
SupportedUILocales: []language.Tag{language.English},
}
handler, err := op.NewDynamicOpenIDProvider("/", config, storage,
//we must explicitly allow the use of the http issuer
op.WithAllowInsecure(),
//as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
)
if err != nil {
return nil, err
}
return handler, nil
}
golang-github-zitadel-oidc-3.27.0/example/server/exampleop/ 0000775 0000000 0000000 00000000000 14656014552 0023643 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/server/exampleop/device.go 0000664 0000000 0000000 00000012110 14656014552 0025424 0 ustar 00root root 0000000 0000000 package exampleop
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"github.com/go-chi/chi/v5"
"github.com/gorilla/securecookie"
"github.com/sirupsen/logrus"
"github.com/zitadel/oidc/v3/pkg/op"
)
type deviceAuthenticate interface {
CheckUsernamePasswordSimple(username, password string) error
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 {
storage deviceAuthenticate
cookie *securecookie.SecureCookie
}
func registerDeviceAuth(storage deviceAuthenticate, router chi.Router) {
l := &deviceLogin{
storage: storage,
cookie: securecookie.New(securecookie.GenerateRandomKey(32), nil),
}
router.HandleFunc("/", l.userCodeHandler)
router.Post("/login", l.loginHandler)
router.HandleFunc("/confirm", l.confirmHandler)
}
func renderUserCode(w io.Writer, err error) {
data := struct {
Error string
}{
Error: errMsg(err),
}
if err := templates.ExecuteTemplate(w, "usercode", data); err != nil {
logrus.Error(err)
}
}
func renderDeviceLogin(w http.ResponseWriter, userCode string, err error) {
data := &struct {
UserCode string
Error string
}{
UserCode: userCode,
Error: errMsg(err),
}
if err = templates.ExecuteTemplate(w, "device_login", data); err != nil {
logrus.Error(err)
}
}
func renderConfirmPage(w http.ResponseWriter, username, clientID string, scopes []string) {
data := &struct {
Username string
ClientID string
Scopes []string
}{
Username: username,
ClientID: clientID,
Scopes: scopes,
}
if err := templates.ExecuteTemplate(w, "confirm_device", data); err != nil {
logrus.Error(err)
}
}
func (d *deviceLogin) userCodeHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
renderUserCode(w, err)
return
}
userCode := r.Form.Get("user_code")
if userCode == "" {
if prompt, _ := url.QueryUnescape(r.Form.Get("prompt")); prompt != "" {
err = errors.New(prompt)
}
renderUserCode(w, err)
return
}
renderDeviceLogin(w, userCode, nil)
}
func redirectBack(w http.ResponseWriter, r *http.Request, prompt string) {
values := make(url.Values)
values.Set("prompt", url.QueryEscape(prompt))
url := url.URL{
Path: "/device",
RawQuery: values.Encode(),
}
http.Redirect(w, r, url.String(), http.StatusSeeOther)
}
const userCodeCookieName = "user_code"
type userCodeCookie struct {
UserCode string
UserName string
}
func (d *deviceLogin) loginHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
redirectBack(w, r, err.Error())
return
}
userCode := r.PostForm.Get("user_code")
if userCode == "" {
redirectBack(w, r, "missing user_code in request")
return
}
username := r.PostForm.Get("username")
if username == "" {
redirectBack(w, r, "missing username in request")
return
}
password := r.PostForm.Get("password")
if password == "" {
redirectBack(w, r, "missing password in request")
return
}
if err := d.storage.CheckUsernamePasswordSimple(username, password); err != nil {
redirectBack(w, r, err.Error())
return
}
state, err := d.storage.GetDeviceAuthorizationByUserCode(r.Context(), userCode)
if err != nil {
redirectBack(w, r, err.Error())
return
}
encoded, err := d.cookie.Encode(userCodeCookieName, userCodeCookie{userCode, username})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
cookie := &http.Cookie{
Name: userCodeCookieName,
Value: encoded,
Path: "/",
}
http.SetCookie(w, cookie)
renderConfirmPage(w, username, state.ClientID, state.Scopes)
}
func (d *deviceLogin) confirmHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(userCodeCookieName)
if err != nil {
redirectBack(w, r, err.Error())
return
}
data := new(userCodeCookie)
if err = d.cookie.Decode(userCodeCookieName, cookie.Value, &data); err != nil {
redirectBack(w, r, err.Error())
return
}
if err = r.ParseForm(); err != nil {
redirectBack(w, r, err.Error())
return
}
action := r.Form.Get("action")
switch action {
case "allowed":
err = d.storage.CompleteDeviceAuthorization(r.Context(), data.UserCode, data.UserName)
case "denied":
err = d.storage.DenyDeviceAuthorization(r.Context(), data.UserCode)
default:
err = errors.New("action must be one of \"allow\" or \"deny\"")
}
if err != nil {
redirectBack(w, r, err.Error())
return
}
fmt.Fprintf(w, "Device authorization %s. You can now return to the device", action)
}
golang-github-zitadel-oidc-3.27.0/example/server/exampleop/login.go 0000664 0000000 0000000 00000004022 14656014552 0025300 0 ustar 00root root 0000000 0000000 package exampleop
import (
"context"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/zitadel/oidc/v3/pkg/op"
)
type login struct {
authenticate authenticate
router chi.Router
callback func(context.Context, string) string
}
func NewLogin(authenticate authenticate, callback func(context.Context, string) string, issuerInterceptor *op.IssuerInterceptor) *login {
l := &login{
authenticate: authenticate,
callback: callback,
}
l.createRouter(issuerInterceptor)
return l
}
func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) {
l.router = chi.NewRouter()
l.router.Get("/username", l.loginHandler)
l.router.Post("/username", issuerInterceptor.HandlerFunc(l.checkLoginHandler))
}
type authenticate interface {
CheckUsernamePassword(username, password, id string) error
}
func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError)
return
}
// the oidc package will pass the id of the auth request as query parameter
// we will use this id through the login process and therefore pass it to the login page
renderLogin(w, r.FormValue(queryAuthRequestID), nil)
}
func renderLogin(w http.ResponseWriter, id string, err error) {
data := &struct {
ID string
Error string
}{
ID: id,
Error: errMsg(err),
}
err = templates.ExecuteTemplate(w, "login", data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
id := r.FormValue("id")
err = l.authenticate.CheckUsernamePassword(username, password, id)
if err != nil {
renderLogin(w, id, err)
return
}
http.Redirect(w, r, l.callback(r.Context(), id), http.StatusFound)
}
golang-github-zitadel-oidc-3.27.0/example/server/exampleop/op.go 0000664 0000000 0000000 00000011413 14656014552 0024610 0 ustar 00root root 0000000 0000000 package exampleop
import (
"crypto/sha256"
"log"
"log/slog"
"net/http"
"sync/atomic"
"time"
"github.com/go-chi/chi/v5"
"github.com/zitadel/logging"
"golang.org/x/text/language"
"github.com/zitadel/oidc/v3/example/server/storage"
"github.com/zitadel/oidc/v3/pkg/op"
)
const (
pathLoggedOut = "/logged-out"
)
func init() {
storage.RegisterClients(
storage.NativeClient("native"),
storage.WebClient("web", "secret"),
storage.WebClient("api", "secret"),
)
}
type Storage interface {
op.Storage
authenticate
deviceAuthenticate
}
// simple counter for request IDs
var counter atomic.Int64
// SetupServer creates an OIDC server with Issuer=http://localhost:
//
// 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 {
// the OpenID Provider requires a 32-byte key for (token) encryption
// be sure to create a proper crypto random key and manage it securely!
key := sha256.Sum256([]byte("test"))
router := chi.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
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("signed out successfully"))
// no need to check/log error, this will be handled by the middleware.
})
// creation of the OpenIDProvider with the just created in-memory Storage
provider, err := newOP(storage, issuer, key, logger, extraOptions...)
if err != nil {
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
//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), op.NewIssuerInterceptor(provider.IssuerFromRequest))
// 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
router.Mount("/login/", http.StripPrefix("/login", l.router))
router.Route("/device", func(r chi.Router) {
registerDeviceAuth(storage, r)
})
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)
// is served on the correct 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/)
router.Mount("/", handler)
return router
}
// newOP will create an OpenID Provider for localhost on a specified port with a given encryption key
// and a predefined default logout uri
// 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) {
config := &op.Config{
CryptoKey: key,
// will be used if the end_session endpoint is called without a post_logout_redirect_uri
DefaultLogoutRedirectURI: pathLoggedOut,
// enables code_challenge_method S256 for PKCE (and therefore PKCE in general)
CodeMethodS256: true,
// enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth)
AuthMethodPost: true,
// enables additional authentication by using private_key_jwt
AuthMethodPrivateKeyJWT: true,
// enables refresh_token grant use
GrantTypeRefreshToken: true,
// enables use of the `request` Object parameter
RequestObjectSupported: true,
// this example has only static texts (in English), so we'll set the here accordingly
SupportedUILocales: []language.Tag{language.English},
DeviceAuthorization: op.DeviceAuthorizationConfig{
Lifetime: 5 * time.Minute,
PollInterval: 5 * time.Second,
UserFormPath: "/device",
UserCode: op.UserCodeBase20,
},
}
handler, err := op.NewOpenIDProvider(issuer, config, storage,
append([]op.Option{
//we must explicitly allow the use of the http issuer
op.WithAllowInsecure(),
// as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
// Pass our logger to the OP
op.WithLogger(logger.WithGroup("op")),
}, extraOptions...)...,
)
if err != nil {
return nil, err
}
return handler, nil
}
golang-github-zitadel-oidc-3.27.0/example/server/exampleop/templates.go 0000664 0000000 0000000 00000000564 14656014552 0026175 0 ustar 00root root 0000000 0000000 package exampleop
import (
"embed"
"html/template"
"github.com/sirupsen/logrus"
)
var (
//go:embed templates
templateFS embed.FS
templates = template.Must(template.ParseFS(templateFS, "templates/*.html"))
)
const (
queryAuthRequestID = "authRequestID"
)
func errMsg(err error) string {
if err == nil {
return ""
}
logrus.Error(err)
return err.Error()
}
golang-github-zitadel-oidc-3.27.0/example/server/exampleop/templates/ 0000775 0000000 0000000 00000000000 14656014552 0025641 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/server/exampleop/templates/confirm_device.html 0000664 0000000 0000000 00000001376 14656014552 0031512 0 ustar 00root root 0000000 0000000 {{ define "confirm_device" -}}
Confirm device authorization
Welcome back {{.Username}}!
You are about to grant device {{.ClientID}} access to the following scopes: {{.Scopes}}.
{{- end }}
golang-github-zitadel-oidc-3.27.0/example/server/exampleop/templates/device_login.html 0000664 0000000 0000000 00000001576 14656014552 0031167 0 ustar 00root root 0000000 0000000 {{ define "device_login" -}}
Login
{{- end }}
golang-github-zitadel-oidc-3.27.0/example/server/exampleop/templates/login.html 0000664 0000000 0000000 00000001554 14656014552 0027644 0 ustar 00root root 0000000 0000000 {{ define "login" -}}
Login
`
{{- end }} golang-github-zitadel-oidc-3.27.0/example/server/exampleop/templates/usercode.html 0000664 0000000 0000000 00000001247 14656014552 0030344 0 ustar 00root root 0000000 0000000 {{ define "usercode" -}}
Device authorization
{{- end }}
golang-github-zitadel-oidc-3.27.0/example/server/main.go 0000664 0000000 0000000 00000002137 14656014552 0023127 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"log/slog"
"net/http"
"os"
"github.com/zitadel/oidc/v3/example/server/exampleop"
"github.com/zitadel/oidc/v3/example/server/storage"
)
func main() {
//we will run on :9998
port := "9998"
//which gives us the issuer: http://localhost:9998/
issuer := fmt.Sprintf("http://localhost:%s/", port)
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
// this might be the layer for accessing your database
// in this example it will be handled in-memory
storage := storage.NewStorage(storage.NewUserStore(issuer))
logger := slog.New(
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}),
)
router := exampleop.SetupServer(issuer, storage, logger, false)
server := &http.Server{
Addr: ":" + port,
Handler: router,
}
logger.Info("server listening, press ctrl+c to stop", "addr", fmt.Sprintf("http://localhost:%s/", port))
err := server.ListenAndServe()
if err != http.ErrServerClosed {
logger.Error("server terminated", "error", err)
os.Exit(1)
}
}
golang-github-zitadel-oidc-3.27.0/example/server/service-key1.json 0000664 0000000 0000000 00000001717 14656014552 0025061 0 ustar 00root root 0000000 0000000 {"type":"serviceaccount","keyId":"key1","key":"-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQD21E+180rCAzp15zy2X/JOYYHtxYhF51pWCsITeChJd7sFWxp1\ntxSHTiomQYBiBWgcCavsdu/VLPQJhO3PTIyglxc1XRGsM48oDT5MkFsAVDvbjuWk\nF0lstQyw4pr8Wg0Ecf1aL6YlvVKB9h5rAgZ9T+elNJ7q5takMAvNhu7zMQIDAQAB\nAoGAeLRw2qjEaUZM43WWchVPmFcEw/MyZgTyX1tZd03uXacolUDtGp3ScyydXiHw\nF39PX063fabYOCaInNMdvJ9RsQz2OcZuS/K6NOmWhzBfLgs4Y1tU6ijoY/gBjHgu\nCV0KjvoWIfEtKl/On/wTrAnUStFzrc7U4dpKFP1fy2ZTTnECQQD8aP2QOxmKUyfg\nBAjfonpkrNeaTRNwTULTvEHFiLyaeFd1PAvsDiKZtpk6iHLb99mQZkVVtAK5qgQ4\n1OI72jkVAkEA+lcAamuZAM+gIiUhbHA7BfX9OVgyGDD2tx5g/kxhMUmK6hIiO6Ul\n0nw5KfrCEUU3AzrM7HejUg3q61SYcXTgrQJBALhrzbhwNf0HPP9Ec2dSw7KDRxSK\ndEV9bfJefn/hpEwI2X3i3aMfwNAmxlYqFCH8OY5z6vzvhX46ZtNPV+z7SPECQQDq\nApXi5P27YlpgULEzup2R7uZsymLZdjvJ5V3pmOBpwENYlublNnVqkrCk60CqADdy\nj26rxRIoS9ZDcWqm9AhpAkEAyrNXBMJh08ghBMb3NYPFfr/bftRJSrGjhBPuJ5qr\nXzWaXhYVMMh3OSAwzHBJbA1ffdQJuH2ebL99Ur5fpBcbVw==\n-----END RSA PRIVATE KEY-----\n","userId":"service"}
golang-github-zitadel-oidc-3.27.0/example/server/storage/ 0000775 0000000 0000000 00000000000 14656014552 0023315 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/example/server/storage/client.go 0000664 0000000 0000000 00000021206 14656014552 0025123 0 ustar 00root root 0000000 0000000 package storage
import (
"time"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
)
var (
// we use the default login UI and pass the (auth request) id
defaultLoginURL = func(id string) string {
return "/login/username?authRequestID=" + id
}
// clients to be used by the storage interface
clients = map[string]*Client{}
)
// Client represents the storage model of an OAuth/OIDC client
// this could also be your database model
type Client struct {
id string
secret string
redirectURIs []string
applicationType op.ApplicationType
authMethod oidc.AuthMethod
loginURL func(string) string
responseTypes []oidc.ResponseType
grantTypes []oidc.GrantType
accessTokenType op.AccessTokenType
devMode bool
idTokenUserinfoClaimsAssertion bool
clockSkew time.Duration
postLogoutRedirectURIGlobs []string
redirectURIGlobs []string
}
// GetID must return the client_id
func (c *Client) GetID() string {
return c.id
}
// RedirectURIs must return the registered redirect_uris for Code and Implicit Flow
func (c *Client) RedirectURIs() []string {
return c.redirectURIs
}
// PostLogoutRedirectURIs must return the registered post_logout_redirect_uris for sign-outs
func (c *Client) PostLogoutRedirectURIs() []string {
return []string{}
}
// ApplicationType must return the type of the client (app, native, user agent)
func (c *Client) ApplicationType() op.ApplicationType {
return c.applicationType
}
// AuthMethod must return the authentication method (client_secret_basic, client_secret_post, none, private_key_jwt)
func (c *Client) AuthMethod() oidc.AuthMethod {
return c.authMethod
}
// ResponseTypes must return all allowed response types (code, id_token token, id_token)
// these must match with the allowed grant types
func (c *Client) ResponseTypes() []oidc.ResponseType {
return c.responseTypes
}
// GrantTypes must return all allowed grant types (authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:jwt-bearer)
func (c *Client) GrantTypes() []oidc.GrantType {
return c.grantTypes
}
// LoginURL will be called to redirect the user (agent) to the login UI
// you could implement some logic here to redirect the users to different login UIs depending on the client
func (c *Client) LoginURL(id string) string {
return c.loginURL(id)
}
// AccessTokenType must return the type of access token the client uses (Bearer (opaque) or JWT)
func (c *Client) AccessTokenType() op.AccessTokenType {
return c.accessTokenType
}
// IDTokenLifetime must return the lifetime of the client's id_tokens
func (c *Client) IDTokenLifetime() time.Duration {
return 1 * time.Hour
}
// DevMode enables the use of non-compliant configs such as redirect_uris (e.g. http schema for user agent client)
func (c *Client) DevMode() bool {
return c.devMode
}
// RestrictAdditionalIdTokenScopes allows specifying which custom scopes shall be asserted into the id_token
func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string {
return scopes
}
}
// RestrictAdditionalAccessTokenScopes allows specifying which custom scopes shall be asserted into the JWT access_token
func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string {
return scopes
}
}
// IsScopeAllowed enables Client specific custom scopes validation
// in this example we allow the CustomScope for all clients
func (c *Client) IsScopeAllowed(scope string) bool {
return scope == CustomScope
}
// IDTokenUserinfoClaimsAssertion allows specifying if claims of scope profile, email, phone and address are asserted into the id_token
// even if an access token if issued which violates the OIDC Core spec
// (5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims)
// some clients though require that e.g. email is always in the id_token when requested even if an access_token is issued
func (c *Client) IDTokenUserinfoClaimsAssertion() bool {
return c.idTokenUserinfoClaimsAssertion
}
// ClockSkew enables clients to instruct the OP to apply a clock skew on the various times and expirations
// (subtract from issued_at, add to expiration, ...)
func (c *Client) ClockSkew() time.Duration {
return c.clockSkew
}
// RegisterClients enables you to register clients for the example implementation
// there are some clients (web and native) to try out different cases
// add more if necessary
//
// RegisterClients should be called before the Storage is used so that there are
// no race conditions.
func RegisterClients(registerClients ...*Client) {
for _, client := range registerClients {
clients[client.id] = client
}
}
// NativeClient will create a client of type native, which will always use PKCE and allow the use of refresh tokens
// user-defined redirectURIs may include:
// - http://localhost without port specification (e.g. http://localhost/auth/callback)
// - custom protocol (e.g. custom://auth/callback)
// (the examples will be used as default, if none is provided)
func NativeClient(id string, redirectURIs ...string) *Client {
if len(redirectURIs) == 0 {
redirectURIs = []string{
"http://localhost/auth/callback",
"custom://auth/callback",
}
}
return &Client{
id: id,
secret: "", // no secret needed (due to PKCE)
redirectURIs: redirectURIs,
applicationType: op.ApplicationTypeNative,
authMethod: oidc.AuthMethodNone,
loginURL: defaultLoginURL,
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
accessTokenType: op.AccessTokenTypeBearer,
devMode: false,
idTokenUserinfoClaimsAssertion: false,
clockSkew: 0,
}
}
// WebClient will create a client of type web, which will always use Basic Auth and allow the use of refresh tokens
// user-defined redirectURIs may include:
// - http://localhost with port specification (e.g. http://localhost:9999/auth/callback)
// (the example will be used as default, if none is provided)
func WebClient(id, secret string, redirectURIs ...string) *Client {
if len(redirectURIs) == 0 {
redirectURIs = []string{
"http://localhost:9999/auth/callback",
}
}
return &Client{
id: id,
secret: secret,
redirectURIs: redirectURIs,
applicationType: op.ApplicationTypeWeb,
authMethod: oidc.AuthMethodBasic,
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},
grantTypes: []oidc.GrantType{oidc.GrantTypeDeviceCode},
accessTokenType: op.AccessTokenTypeBearer,
devMode: false,
idTokenUserinfoClaimsAssertion: false,
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
}
golang-github-zitadel-oidc-3.27.0/example/server/storage/oidc.go 0000664 0000000 0000000 00000012154 14656014552 0024565 0 ustar 00root root 0000000 0000000 package storage
import (
"log/slog"
"time"
"golang.org/x/text/language"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
)
const (
// CustomScope is an example for how to use custom scopes in this library
//(in this scenario, when requested, it will return a custom claim)
CustomScope = "custom_scope"
// CustomClaim is an example for how to return custom claims with this library
CustomClaim = "custom_claim"
// CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchage
CustomScopeImpersonatePrefix = "custom_scope:impersonate:"
)
type AuthRequest struct {
ID string
CreationDate time.Time
ApplicationID string
CallbackURI string
TransferState string
Prompt []string
UiLocales []language.Tag
LoginHint string
MaxAuthAge *time.Duration
UserID string
Scopes []string
ResponseType oidc.ResponseType
ResponseMode oidc.ResponseMode
Nonce string
CodeChallenge *OIDCCodeChallenge
done bool
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 {
return a.ID
}
func (a *AuthRequest) GetACR() string {
return "" // we won't handle acr in this example
}
func (a *AuthRequest) GetAMR() []string {
// this example only uses password for authentication
if a.done {
return []string{"pwd"}
}
return nil
}
func (a *AuthRequest) GetAudience() []string {
return []string{a.ApplicationID} // this example will always just use the client_id as audience
}
func (a *AuthRequest) GetAuthTime() time.Time {
return a.authTime
}
func (a *AuthRequest) GetClientID() string {
return a.ApplicationID
}
func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge {
return CodeChallengeToOIDC(a.CodeChallenge)
}
func (a *AuthRequest) GetNonce() string {
return a.Nonce
}
func (a *AuthRequest) GetRedirectURI() string {
return a.CallbackURI
}
func (a *AuthRequest) GetResponseType() oidc.ResponseType {
return a.ResponseType
}
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
return a.ResponseMode
}
func (a *AuthRequest) GetScopes() []string {
return a.Scopes
}
func (a *AuthRequest) GetState() string {
return a.TransferState
}
func (a *AuthRequest) GetSubject() string {
return a.UserID
}
func (a *AuthRequest) Done() bool {
return a.done
}
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
prompts := make([]string, len(oidcPrompt))
for _, oidcPrompt := range oidcPrompt {
switch oidcPrompt {
case oidc.PromptNone,
oidc.PromptLogin,
oidc.PromptConsent,
oidc.PromptSelectAccount:
prompts = append(prompts, oidcPrompt)
}
}
return prompts
}
func MaxAgeToInternal(maxAge *uint) *time.Duration {
if maxAge == nil {
return nil
}
dur := time.Duration(*maxAge) * time.Second
return &dur
}
func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthRequest {
return &AuthRequest{
CreationDate: time.Now(),
ApplicationID: authReq.ClientID,
CallbackURI: authReq.RedirectURI,
TransferState: authReq.State,
Prompt: PromptToInternal(authReq.Prompt),
UiLocales: authReq.UILocales,
LoginHint: authReq.LoginHint,
MaxAuthAge: MaxAgeToInternal(authReq.MaxAge),
UserID: userID,
Scopes: authReq.Scopes,
ResponseType: authReq.ResponseType,
ResponseMode: authReq.ResponseMode,
Nonce: authReq.Nonce,
CodeChallenge: &OIDCCodeChallenge{
Challenge: authReq.CodeChallenge,
Method: string(authReq.CodeChallengeMethod),
},
}
}
type OIDCCodeChallenge struct {
Challenge string
Method string
}
func CodeChallengeToOIDC(challenge *OIDCCodeChallenge) *oidc.CodeChallenge {
if challenge == nil {
return nil
}
challengeMethod := oidc.CodeChallengeMethodPlain
if challenge.Method == "S256" {
challengeMethod = oidc.CodeChallengeMethodS256
}
return &oidc.CodeChallenge{
Challenge: challenge.Challenge,
Method: challengeMethod,
}
}
// RefreshTokenRequestFromBusiness will simply wrap the storage RefreshToken to implement the op.RefreshTokenRequest interface
func RefreshTokenRequestFromBusiness(token *RefreshToken) op.RefreshTokenRequest {
return &RefreshTokenRequest{token}
}
type RefreshTokenRequest struct {
*RefreshToken
}
func (r *RefreshTokenRequest) GetAMR() []string {
return r.AMR
}
func (r *RefreshTokenRequest) GetAudience() []string {
return r.Audience
}
func (r *RefreshTokenRequest) GetAuthTime() time.Time {
return r.AuthTime
}
func (r *RefreshTokenRequest) GetClientID() string {
return r.ApplicationID
}
func (r *RefreshTokenRequest) GetScopes() []string {
return r.Scopes
}
func (r *RefreshTokenRequest) GetSubject() string {
return r.UserID
}
func (r *RefreshTokenRequest) SetCurrentScopes(scopes []string) {
r.Scopes = scopes
}
golang-github-zitadel-oidc-3.27.0/example/server/storage/storage.go 0000664 0000000 0000000 00000076707 14656014552 0025331 0 ustar 00root root 0000000 0000000 package storage
import (
"context"
"crypto/rand"
"crypto/rsa"
"errors"
"fmt"
"math/big"
"strings"
"sync"
"time"
jose "github.com/go-jose/go-jose/v4"
"github.com/google/uuid"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
)
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
// the corresponding private key is in the service-key1.json (for demonstration purposes)
var serviceKey1 = &rsa.PublicKey{
N: func() *big.Int {
n, _ := new(big.Int).SetString("00f6d44fb5f34ac2033a75e73cb65ff24e6181edc58845e75a560ac21378284977bb055b1a75b714874e2a2641806205681c09abec76efd52cf40984edcf4c8ca09717355d11ac338f280d3e4c905b00543bdb8ee5a417496cb50cb0e29afc5a0d0471fd5a2fa625bd5281f61e6b02067d4fe7a5349eeae6d6a4300bcd86eef331", 16)
return n
}(),
E: 65537,
}
var (
_ op.Storage = &Storage{}
_ op.ClientCredentialsStorage = &Storage{}
)
// storage implements the op.Storage interface
// typically you would implement this as a layer on top of your database
// for simplicity this example keeps everything in-memory
type Storage struct {
lock sync.Mutex
authRequests map[string]*AuthRequest
codes map[string]string
tokens map[string]*Token
clients map[string]*Client
userStore UserStore
services map[string]Service
refreshTokens map[string]*RefreshToken
signingKey signingKey
deviceCodes map[string]deviceAuthorizationEntry
userCodes map[string]string
serviceUsers map[string]*Client
}
type signingKey struct {
id string
algorithm jose.SignatureAlgorithm
key *rsa.PrivateKey
}
func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm {
return s.algorithm
}
func (s *signingKey) Key() any {
return s.key
}
func (s *signingKey) ID() string {
return s.id
}
type publicKey struct {
signingKey
}
func (s *publicKey) ID() string {
return s.id
}
func (s *publicKey) Algorithm() jose.SignatureAlgorithm {
return s.algorithm
}
func (s *publicKey) Use() string {
return "sig"
}
func (s *publicKey) Key() any {
return &s.key.PublicKey
}
func NewStorage(userStore UserStore) *Storage {
return NewStorageWithClients(userStore, clients)
}
func NewStorageWithClients(userStore UserStore, clients map[string]*Client) *Storage {
key, _ := rsa.GenerateKey(rand.Reader, 2048)
return &Storage{
authRequests: make(map[string]*AuthRequest),
codes: make(map[string]string),
tokens: make(map[string]*Token),
refreshTokens: make(map[string]*RefreshToken),
clients: clients,
userStore: userStore,
services: map[string]Service{
userStore.ExampleClientID(): {
keys: map[string]*rsa.PublicKey{
"key1": serviceKey1,
},
},
},
signingKey: signingKey{
id: uuid.NewString(),
algorithm: jose.RS256,
key: key,
},
deviceCodes: make(map[string]deviceAuthorizationEntry),
userCodes: make(map[string]string),
serviceUsers: map[string]*Client{
"sid1": {
id: "sid1",
secret: "verysecret",
grantTypes: []oidc.GrantType{
oidc.GrantTypeClientCredentials,
},
accessTokenType: op.AccessTokenTypeBearer,
},
},
}
}
// CheckUsernamePassword implements the `authenticate` interface of the login
func (s *Storage) CheckUsernamePassword(username, password, id string) error {
s.lock.Lock()
defer s.lock.Unlock()
request, ok := s.authRequests[id]
if !ok {
return fmt.Errorf("request not found")
}
// for demonstration purposes we'll check we'll have a simple user store and
// a plain text password. For real world scenarios, be sure to have the password
// hashed and salted (e.g. using bcrypt)
user := s.userStore.GetUserByUsername(username)
if user != nil && user.Password == password {
// be sure to set user id into the auth request after the user was checked,
// so that you'll be able to get more information about the user after the login
request.UserID = user.ID
// 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
// therefore we will also just check this boolean if the request / login has been finished
request.done = true
return nil
}
return fmt.Errorf("username or password wrong")
}
func (s *Storage) CheckUsernamePasswordSimple(username, password string) error {
s.lock.Lock()
defer s.lock.Unlock()
user := s.userStore.GetUserByUsername(username)
if user != nil && user.Password == password {
return nil
}
return fmt.Errorf("username or password wrong")
}
// CreateAuthRequest implements the op.Storage interface
// it will be called after parsing and validation of the authentication request
func (s *Storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) {
s.lock.Lock()
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
request := authRequestToInternal(authReq, userID)
// you'll also have to create a unique id for the request (this might be done by your database; we'll use a uuid)
request.ID = uuid.NewString()
// and save it in your database (for demonstration purposed we will use a simple map)
s.authRequests[request.ID] = request
// finally, return the request (which implements the AuthRequest interface of the OP
return request, nil
}
// AuthRequestByID implements the op.Storage interface
// it will be called after the Login UI redirects back to the OIDC endpoint
func (s *Storage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) {
s.lock.Lock()
defer s.lock.Unlock()
request, ok := s.authRequests[id]
if !ok {
return nil, fmt.Errorf("request not found")
}
return request, nil
}
// AuthRequestByCode implements the op.Storage interface
// it will be called after parsing and validation of the token request (in an authorization code flow)
func (s *Storage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) {
// for this example we read the id by code and then get the request by id
requestID, ok := func() (string, bool) {
s.lock.Lock()
defer s.lock.Unlock()
requestID, ok := s.codes[code]
return requestID, ok
}()
if !ok {
return nil, fmt.Errorf("code invalid or expired")
}
return s.AuthRequestByID(ctx, requestID)
}
// SaveAuthCode implements the op.Storage interface
// it will be called after the authentication has been successful and before redirecting the user agent to the redirect_uri
// (in an authorization code flow)
func (s *Storage) SaveAuthCode(ctx context.Context, id string, code string) error {
// for this example we'll just save the authRequestID to the code
s.lock.Lock()
defer s.lock.Unlock()
s.codes[code] = id
return nil
}
// DeleteAuthRequest implements the op.Storage interface
// it will be called after creating the token response (id and access tokens) for a valid
// - authentication request (in an implicit flow)
// - token request (in an authorization code flow)
func (s *Storage) DeleteAuthRequest(ctx context.Context, id string) error {
// you can simply delete all reference to the auth request
s.lock.Lock()
defer s.lock.Unlock()
delete(s.authRequests, id)
for code, requestID := range s.codes {
if id == requestID {
delete(s.codes, code)
return nil
}
}
return nil
}
// CreateAccessToken implements the op.Storage interface
// it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...)
func (s *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) {
var applicationID string
switch req := request.(type) {
case *AuthRequest:
// if authenticated for an app (auth code / implicit flow) we must save the client_id to the token
applicationID = req.ApplicationID
case op.TokenExchangeRequest:
applicationID = req.GetClientID()
}
token, err := s.accessToken(applicationID, "", request.GetSubject(), request.GetAudience(), request.GetScopes())
if err != nil {
return "", time.Time{}, err
}
return token.ID, token.Expiration, nil
}
// CreateAccessAndRefreshTokens implements the op.Storage interface
// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request)
func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
// generate tokens via token exchange flow if request is relevant
if teReq, ok := request.(op.TokenExchangeRequest); ok {
return s.exchangeRefreshToken(ctx, teReq)
}
// get the information depending on the request type / implementation
applicationID, authTime, amr := getInfoFromRequest(request)
// if currentRefreshToken is empty (Code Flow) we will have to create a new refresh token
if currentRefreshToken == "" {
refreshTokenID := uuid.NewString()
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
if err != nil {
return "", "", time.Time{}, err
}
refreshToken, err := s.createRefreshToken(accessToken, amr, authTime)
if err != nil {
return "", "", time.Time{}, err
}
return accessToken.ID, refreshToken, accessToken.Expiration, nil
}
// 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
refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken)
if err != nil {
return "", "", time.Time{}, err
}
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
if err != nil {
return "", "", time.Time{}, err
}
return accessToken.ID, refreshToken, accessToken.Expiration, nil
}
func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
applicationID := request.GetClientID()
authTime := request.GetAuthTime()
refreshTokenID := uuid.NewString()
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
if err != nil {
return "", "", time.Time{}, err
}
refreshToken, err := s.createRefreshToken(accessToken, nil, authTime)
if err != nil {
return "", "", time.Time{}, err
}
return accessToken.ID, refreshToken, accessToken.Expiration, nil
}
// TokenRequestByRefreshToken implements the op.Storage interface
// it will be called after parsing and validation of the refresh token request
func (s *Storage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
s.lock.Lock()
defer s.lock.Unlock()
token, ok := s.refreshTokens[refreshToken]
if !ok {
return nil, fmt.Errorf("invalid refresh_token")
}
return RefreshTokenRequestFromBusiness(token), nil
}
// TerminateSession implements the op.Storage interface
// it will be called after the user signed out, therefore the access and refresh token of the user of this client must be removed
func (s *Storage) TerminateSession(ctx context.Context, userID string, clientID string) error {
s.lock.Lock()
defer s.lock.Unlock()
for _, token := range s.tokens {
if token.ApplicationID == clientID && token.Subject == userID {
delete(s.tokens, token.ID)
delete(s.refreshTokens, token.RefreshTokenID)
}
}
return nil
}
// GetRefreshTokenInfo looks up a refresh token and returns the token id and user id.
// If given something that is not a refresh token, it must return error.
func (s *Storage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) {
refreshToken, ok := s.refreshTokens[token]
if !ok {
return "", "", op.ErrInvalidRefreshToken
}
return refreshToken.UserID, refreshToken.ID, nil
}
// RevokeToken implements the op.Storage interface
// it will be called after parsing and validation of the token revocation request
func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID string, clientID string) *oidc.Error {
// a single token was requested to be removed
s.lock.Lock()
defer s.lock.Unlock()
accessToken, ok := s.tokens[tokenIDOrToken] // tokenID
if ok {
if accessToken.ApplicationID != clientID {
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
}
// if it is an access token, just remove it
// you could also remove the corresponding refresh token if really necessary
delete(s.tokens, accessToken.ID)
return nil
}
refreshToken, ok := s.refreshTokens[tokenIDOrToken] // token
if !ok {
// if the token is neither an access nor a refresh token, just ignore it, the expected behaviour of
// being not valid (anymore) is achieved
return nil
}
if refreshToken.ApplicationID != clientID {
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
}
// if it is a refresh token, you will have to remove the access token as well
delete(s.refreshTokens, refreshToken.ID)
for _, accessToken := range s.tokens {
if accessToken.RefreshTokenID == refreshToken.ID {
delete(s.tokens, accessToken.ID)
return nil
}
}
return nil
}
// SigningKey implements the op.Storage interface
// it will be called when creating the OpenID Provider
func (s *Storage) SigningKey(ctx context.Context) (op.SigningKey, error) {
// in this example the signing key is a static rsa.PrivateKey and the algorithm used is RS256
// you would obviously have a more complex implementation and store / retrieve the key from your database as well
return &s.signingKey, nil
}
// SignatureAlgorithms implements the op.Storage interface
// it will be called to get the sign
func (s *Storage) SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) {
return []jose.SignatureAlgorithm{s.signingKey.algorithm}, nil
}
// KeySet implements the op.Storage interface
// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ...
func (s *Storage) KeySet(ctx context.Context) ([]op.Key, error) {
// as mentioned above, this example only has a single signing key without key rotation,
// so it will directly use its public key
//
// when using key rotation you typically would store the public keys alongside the private keys in your database
// and give both of them an expiration date, with the public key having a longer lifetime
return []op.Key{&publicKey{s.signingKey}}, nil
}
// GetClientByClientID implements the op.Storage interface
// it will be called whenever information (type, redirect_uris, ...) about the client behind the client_id is needed
func (s *Storage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) {
s.lock.Lock()
defer s.lock.Unlock()
client, ok := s.clients[clientID]
if !ok {
return nil, fmt.Errorf("client not found")
}
return RedirectGlobsClient(client), nil
}
// AuthorizeClientIDSecret implements the op.Storage interface
// it will be called for validating the client_id, client_secret on token or introspection requests
func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error {
s.lock.Lock()
defer s.lock.Unlock()
client, ok := s.clients[clientID]
if !ok {
return fmt.Errorf("client not found")
}
// for this example we directly check the secret
// obviously you would not have the secret in plain text, but rather hashed and salted (e.g. using bcrypt)
if client.secret != clientSecret {
return fmt.Errorf("invalid secret")
}
return nil
}
// SetUserinfoFromScopes implements the op.Storage interface.
// Provide an empty implementation and use SetUserinfoFromRequest instead.
func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error {
return nil
}
// 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
// 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 {
token, ok := func() (*Token, bool) {
s.lock.Lock()
defer s.lock.Unlock()
token, ok := s.tokens[tokenID]
return token, ok
}()
if !ok {
return fmt.Errorf("token is invalid or has expired")
}
// the userinfo endpoint should support CORS. If it's not possible to specify a specific origin in the CORS handler,
// and you have to specify a wildcard (*) origin, then you could also check here if the origin which called the userinfo endpoint here directly
// note that the origin can be empty (if called by a web client)
//
// if origin != "" {
// client, ok := s.clients[token.ApplicationID]
// if !ok {
// return fmt.Errorf("client not found")
// }
// if err := checkAllowedOrigins(client.allowedOrigins, origin); err != nil {
// return err
// }
//}
return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes)
}
// 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
func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
token, ok := func() (*Token, bool) {
s.lock.Lock()
defer s.lock.Unlock()
token, ok := s.tokens[tokenID]
return token, ok
}()
if !ok {
return fmt.Errorf("token is invalid or has expired")
}
// check if the client is part of the requested audience
for _, aud := range token.Audience {
if aud == clientID {
// the introspection response only has to return a boolean (active) if the token is active
// 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
// e.g. the userinfo (equivalent to userinfo endpoint)
userInfo := new(oidc.UserInfo)
err := s.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes)
if err != nil {
return err
}
introspection.SetUserInfo(userInfo)
//...and also the requested scopes...
introspection.Scope = token.Scopes
//...and the client the token was issued to
introspection.ClientID = token.ApplicationID
return nil
}
}
return fmt.Errorf("token is not valid for this client")
}
// GetPrivateClaimsFromScopes implements the op.Storage interface
// 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) {
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) {
for _, scope := range scopes {
switch scope {
case CustomScope:
claims = appendClaim(claims, CustomClaim, customClaim(clientID))
}
}
return claims, nil
}
// GetKeyByIDAndClientID implements the op.Storage interface
// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication)
func (s *Storage) GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) {
s.lock.Lock()
defer s.lock.Unlock()
service, ok := s.services[clientID]
if !ok {
return nil, fmt.Errorf("clientID not found")
}
key, ok := service.keys[keyID]
if !ok {
return nil, fmt.Errorf("key not found")
}
return &jose.JSONWebKey{
KeyID: keyID,
Use: "sig",
Key: key,
}, nil
}
// ValidateJWTProfileScopes implements the op.Storage interface
// it will be called to validate the scopes of a JWT Profile Authorization Grant request
func (s *Storage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) {
allowedScopes := make([]string, 0)
for _, scope := range scopes {
if scope == oidc.ScopeOpenID {
allowedScopes = append(allowedScopes, scope)
}
}
return allowedScopes, nil
}
// Health implements the op.Storage interface
func (s *Storage) Health(ctx context.Context) error {
return nil
}
// createRefreshToken will store a refresh_token in-memory based on the provided information
func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime time.Time) (string, error) {
s.lock.Lock()
defer s.lock.Unlock()
token := &RefreshToken{
ID: accessToken.RefreshTokenID,
Token: accessToken.RefreshTokenID,
AuthTime: authTime,
AMR: amr,
ApplicationID: accessToken.ApplicationID,
UserID: accessToken.Subject,
Audience: accessToken.Audience,
Expiration: time.Now().Add(5 * time.Hour),
Scopes: accessToken.Scopes,
}
s.refreshTokens[token.ID] = token
return token.Token, nil
}
// renewRefreshToken checks the provided refresh_token and creates a new one based on the current
func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) {
s.lock.Lock()
defer s.lock.Unlock()
refreshToken, ok := s.refreshTokens[currentRefreshToken]
if !ok {
return "", "", fmt.Errorf("invalid refresh token")
}
// deletes the refresh token and all access tokens which were issued based on this refresh token
delete(s.refreshTokens, currentRefreshToken)
for _, token := range s.tokens {
if token.RefreshTokenID == currentRefreshToken {
delete(s.tokens, token.ID)
break
}
}
// creates a new refresh token based on the current one
token := uuid.NewString()
refreshToken.Token = token
refreshToken.ID = token
s.refreshTokens[token] = refreshToken
return token, refreshToken.ID, nil
}
// accessToken will store an access_token in-memory based on the provided information
func (s *Storage) accessToken(applicationID, refreshTokenID, subject string, audience, scopes []string) (*Token, error) {
s.lock.Lock()
defer s.lock.Unlock()
token := &Token{
ID: uuid.NewString(),
ApplicationID: applicationID,
RefreshTokenID: refreshTokenID,
Subject: subject,
Audience: audience,
Expiration: time.Now().Add(5 * time.Minute),
Scopes: scopes,
}
s.tokens[token.ID] = token
return token, nil
}
// 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) {
s.lock.Lock()
defer s.lock.Unlock()
user := s.userStore.GetUserByID(userID)
if user == nil {
return fmt.Errorf("user not found")
}
for _, scope := range scopes {
switch scope {
case oidc.ScopeOpenID:
userInfo.Subject = user.ID
case oidc.ScopeEmail:
userInfo.Email = user.Email
userInfo.EmailVerified = oidc.Bool(user.EmailVerified)
case oidc.ScopeProfile:
userInfo.PreferredUsername = user.Username
userInfo.Name = user.FirstName + " " + user.LastName
userInfo.FamilyName = user.LastName
userInfo.GivenName = user.FirstName
userInfo.Locale = oidc.NewLocale(user.PreferredLanguage)
case oidc.ScopePhone:
userInfo.PhoneNumber = user.Phone
userInfo.PhoneNumberVerified = user.PhoneVerified
case CustomScope:
// you can also have a custom scope and assert public or custom claims based on that
userInfo.AppendClaims(CustomClaim, customClaim(clientID))
}
}
return nil
}
// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface
// it will be called to validate parsed Token Exchange Grant request
func (s *Storage) ValidateTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) error {
if request.GetRequestedTokenType() == "" {
request.SetRequestedTokenType(oidc.RefreshTokenType)
}
// Just an example, some use cases might need this use case
if request.GetExchangeSubjectTokenType() == oidc.IDTokenType && request.GetRequestedTokenType() == oidc.RefreshTokenType {
return errors.New("exchanging id_token to refresh_token is not supported")
}
// Check impersonation permissions
if request.GetExchangeActor() == "" && !s.userStore.GetUserByID(request.GetExchangeSubject()).IsAdmin {
return errors.New("user doesn't have impersonation permission")
}
allowedScopes := make([]string, 0)
for _, scope := range request.GetScopes() {
if scope == oidc.ScopeAddress {
continue
}
if strings.HasPrefix(scope, CustomScopeImpersonatePrefix) {
subject := strings.TrimPrefix(scope, CustomScopeImpersonatePrefix)
request.SetSubject(subject)
}
allowedScopes = append(allowedScopes, scope)
}
request.SetCurrentScopes(allowedScopes)
return nil
}
// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface
// Common use case is to store request for audit purposes. For this example we skip the storing.
func (s *Storage) CreateTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) error {
return nil
}
// 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
// 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) {
claims, err = s.getPrivateClaimsFromScopes(ctx, "", request.GetClientID(), request.GetScopes())
if err != nil {
return nil, err
}
for k, v := range s.getTokenExchangeClaims(ctx, request) {
claims = appendClaim(claims, k, v)
}
return claims, nil
}
// 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,
// 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 {
err := s.setUserinfo(ctx, userinfo, request.GetSubject(), request.GetClientID(), request.GetScopes())
if err != nil {
return err
}
for k, v := range s.getTokenExchangeClaims(ctx, request) {
userinfo.AppendClaims(k, v)
}
return nil
}
func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]any) {
for _, scope := range request.GetScopes() {
switch {
case strings.HasPrefix(scope, CustomScopeImpersonatePrefix) && request.GetExchangeActor() == "":
// Set actor subject claim for impersonation flow
claims = appendClaim(claims, "act", map[string]any{
"sub": request.GetExchangeSubject(),
})
}
}
// Set actor subject claim for delegation flow
// if request.GetExchangeActor() != "" {
// claims = appendClaim(claims, "act", map[string]any{
// "sub": request.GetExchangeActor(),
// })
// }
return claims
}
// getInfoFromRequest returns the clientID, authTime and amr depending on the op.TokenRequest type / implementation
func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Time, amr []string) {
authReq, ok := req.(*AuthRequest) // Code Flow (with scope offline_access)
if ok {
return authReq.ApplicationID, authReq.authTime, authReq.GetAMR()
}
refreshReq, ok := req.(*RefreshTokenRequest) // Refresh Token Request
if ok {
return refreshReq.ApplicationID, refreshReq.AuthTime, refreshReq.AMR
}
return "", time.Time{}, nil
}
// customClaim demonstrates how to return custom claims based on provided information
func customClaim(clientID string) map[string]any {
return map[string]any{
"client": clientID,
"other": "stuff",
}
}
func appendClaim(claims map[string]any, claim string, value any) map[string]any {
if claims == nil {
claims = make(map[string]any)
}
claims[claim] = value
return claims
}
type deviceAuthorizationEntry struct {
deviceCode string
userCode string
state *op.DeviceAuthorizationState
}
func (s *Storage) StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) error {
s.lock.Lock()
defer s.lock.Unlock()
if _, ok := s.clients[clientID]; !ok {
return errors.New("client not found")
}
if _, ok := s.userCodes[userCode]; ok {
return op.ErrDuplicateUserCode
}
s.deviceCodes[deviceCode] = deviceAuthorizationEntry{
deviceCode: deviceCode,
userCode: userCode,
state: &op.DeviceAuthorizationState{
ClientID: clientID,
Scopes: scopes,
Expires: expires,
},
}
s.userCodes[userCode] = deviceCode
return nil
}
func (s *Storage) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*op.DeviceAuthorizationState, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
s.lock.Lock()
defer s.lock.Unlock()
entry, ok := s.deviceCodes[deviceCode]
if !ok || entry.state.ClientID != clientID {
return nil, errors.New("device code not found for client") // is there a standard not found error in the framework?
}
return entry.state, nil
}
func (s *Storage) GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error) {
s.lock.Lock()
defer s.lock.Unlock()
entry, ok := s.deviceCodes[s.userCodes[userCode]]
if !ok {
return nil, errors.New("user code not found")
}
return entry.state, nil
}
func (s *Storage) CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error {
s.lock.Lock()
defer s.lock.Unlock()
entry, ok := s.deviceCodes[s.userCodes[userCode]]
if !ok {
return errors.New("user code not found")
}
entry.state.Subject = subject
entry.state.Done = true
return nil
}
func (s *Storage) DenyDeviceAuthorization(ctx context.Context, userCode string) error {
s.lock.Lock()
defer s.lock.Unlock()
s.deviceCodes[s.userCodes[userCode]].state.Denied = true
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
}
golang-github-zitadel-oidc-3.27.0/example/server/storage/storage_dynamic.go 0000664 0000000 0000000 00000025565 14656014552 0027031 0 ustar 00root root 0000000 0000000 package storage
import (
"context"
"time"
jose "github.com/go-jose/go-jose/v4"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
)
type multiStorage struct {
issuers map[string]*Storage
}
// NewMultiStorage implements the op.Storage interface by wrapping multiple storage structs
// and selecting them by the calling issuer
func NewMultiStorage(issuers []string) *multiStorage {
s := make(map[string]*Storage)
for _, issuer := range issuers {
s[issuer] = NewStorage(NewUserStore(issuer))
}
return &multiStorage{issuers: s}
}
// CheckUsernamePassword implements the `authenticate` interface of the login
func (s *multiStorage) CheckUsernamePassword(ctx context.Context, username, password, id string) error {
storage, err := s.storageFromContext(ctx)
if err != nil {
return err
}
return storage.CheckUsernamePassword(username, password, id)
}
// CreateAuthRequest implements the op.Storage interface
// it will be called after parsing and validation of the authentication request
func (s *multiStorage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.CreateAuthRequest(ctx, authReq, userID)
}
// AuthRequestByID implements the op.Storage interface
// it will be called after the Login UI redirects back to the OIDC endpoint
func (s *multiStorage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.AuthRequestByID(ctx, id)
}
// AuthRequestByCode implements the op.Storage interface
// it will be called after parsing and validation of the token request (in an authorization code flow)
func (s *multiStorage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.AuthRequestByCode(ctx, code)
}
// SaveAuthCode implements the op.Storage interface
// it will be called after the authentication has been successful and before redirecting the user agent to the redirect_uri
// (in an authorization code flow)
func (s *multiStorage) SaveAuthCode(ctx context.Context, id string, code string) error {
storage, err := s.storageFromContext(ctx)
if err != nil {
return err
}
return storage.SaveAuthCode(ctx, id, code)
}
// DeleteAuthRequest implements the op.Storage interface
// it will be called after creating the token response (id and access tokens) for a valid
// - authentication request (in an implicit flow)
// - token request (in an authorization code flow)
func (s *multiStorage) DeleteAuthRequest(ctx context.Context, id string) error {
storage, err := s.storageFromContext(ctx)
if err != nil {
return err
}
return storage.DeleteAuthRequest(ctx, id)
}
// CreateAccessToken implements the op.Storage interface
// it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...)
func (s *multiStorage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return "", time.Time{}, err
}
return storage.CreateAccessToken(ctx, request)
}
// CreateAccessAndRefreshTokens implements the op.Storage interface
// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request)
func (s *multiStorage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return "", "", time.Time{}, err
}
return storage.CreateAccessAndRefreshTokens(ctx, request, currentRefreshToken)
}
// TokenRequestByRefreshToken implements the op.Storage interface
// it will be called after parsing and validation of the refresh token request
func (s *multiStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.TokenRequestByRefreshToken(ctx, refreshToken)
}
// TerminateSession implements the op.Storage interface
// it will be called after the user signed out, therefore the access and refresh token of the user of this client must be removed
func (s *multiStorage) TerminateSession(ctx context.Context, userID string, clientID string) error {
storage, err := s.storageFromContext(ctx)
if err != nil {
return err
}
return storage.TerminateSession(ctx, userID, clientID)
}
// GetRefreshTokenInfo looks up a refresh token and returns the token id and user id.
// If given something that is not a refresh token, it must return error.
func (s *multiStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return "", "", err
}
return storage.GetRefreshTokenInfo(ctx, clientID, token)
}
// RevokeToken implements the op.Storage interface
// it will be called after parsing and validation of the token revocation request
func (s *multiStorage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error {
storage, err := s.storageFromContext(ctx)
if err != nil {
return err
}
return storage.RevokeToken(ctx, token, userID, clientID)
}
// SigningKey implements the op.Storage interface
// it will be called when creating the OpenID Provider
func (s *multiStorage) SigningKey(ctx context.Context) (op.SigningKey, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.SigningKey(ctx)
}
// SignatureAlgorithms implements the op.Storage interface
// it will be called to get the sign
func (s *multiStorage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgorithm, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.SignatureAlgorithms(ctx)
}
// KeySet implements the op.Storage interface
// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ...
func (s *multiStorage) KeySet(ctx context.Context) ([]op.Key, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.KeySet(ctx)
}
// GetClientByClientID implements the op.Storage interface
// it will be called whenever information (type, redirect_uris, ...) about the client behind the client_id is needed
func (s *multiStorage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.GetClientByClientID(ctx, clientID)
}
// AuthorizeClientIDSecret implements the op.Storage interface
// it will be called for validating the client_id, client_secret on token or introspection requests
func (s *multiStorage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error {
storage, err := s.storageFromContext(ctx)
if err != nil {
return err
}
return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret)
}
// SetUserinfoFromScopes implements the op.Storage interface.
// Provide an empty implementation and use SetUserinfoFromRequest instead.
func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error {
storage, err := s.storageFromContext(ctx)
if err != nil {
return err
}
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
// 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 {
storage, err := s.storageFromContext(ctx)
if err != nil {
return err
}
return storage.SetUserinfoFromToken(ctx, userinfo, tokenID, subject, origin)
}
// 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
func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
storage, err := s.storageFromContext(ctx)
if err != nil {
return err
}
return storage.SetIntrospectionFromToken(ctx, introspection, tokenID, subject, clientID)
}
// GetPrivateClaimsFromScopes implements the op.Storage interface
// 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) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.GetPrivateClaimsFromScopes(ctx, userID, clientID, scopes)
}
// GetKeyByIDAndClientID implements the op.Storage interface
// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication)
func (s *multiStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.GetKeyByIDAndClientID(ctx, keyID, userID)
}
// ValidateJWTProfileScopes implements the op.Storage interface
// it will be called to validate the scopes of a JWT Profile Authorization Grant request
func (s *multiStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.ValidateJWTProfileScopes(ctx, userID, scopes)
}
// Health implements the op.Storage interface
func (s *multiStorage) Health(ctx context.Context) error {
return nil
}
func (s *multiStorage) storageFromContext(ctx context.Context) (*Storage, *oidc.Error) {
storage, ok := s.issuers[op.IssuerFromContext(ctx)]
if !ok {
return nil, oidc.ErrInvalidRequest().WithDescription("invalid issuer")
}
return storage, nil
}
golang-github-zitadel-oidc-3.27.0/example/server/storage/token.go 0000664 0000000 0000000 00000000716 14656014552 0024770 0 ustar 00root root 0000000 0000000 package storage
import "time"
type Token struct {
ID string
ApplicationID string
Subject string
RefreshTokenID string
Audience []string
Expiration time.Time
Scopes []string
}
type RefreshToken struct {
ID string
Token string
AuthTime time.Time
AMR []string
Audience []string
UserID string
ApplicationID string
Expiration time.Time
Scopes []string
}
golang-github-zitadel-oidc-3.27.0/example/server/storage/user.go 0000664 0000000 0000000 00000003612 14656014552 0024624 0 ustar 00root root 0000000 0000000 package storage
import (
"crypto/rsa"
"strings"
"golang.org/x/text/language"
)
type User struct {
ID string
Username string
Password string
FirstName string
LastName string
Email string
EmailVerified bool
Phone string
PhoneVerified bool
PreferredLanguage language.Tag
IsAdmin bool
}
type Service struct {
keys map[string]*rsa.PublicKey
}
type UserStore interface {
GetUserByID(string) *User
GetUserByUsername(string) *User
ExampleClientID() string
}
type userStore struct {
users map[string]*User
}
func NewUserStore(issuer string) UserStore {
hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
return userStore{
users: map[string]*User{
"id1": {
ID: "id1",
Username: "test-user@" + hostname,
Password: "verysecure",
FirstName: "Test",
LastName: "User",
Email: "test-user@zitadel.ch",
EmailVerified: true,
Phone: "",
PhoneVerified: false,
PreferredLanguage: language.German,
IsAdmin: true,
},
"id2": {
ID: "id2",
Username: "test-user2",
Password: "verysecure",
FirstName: "Test",
LastName: "User2",
Email: "test-user2@zitadel.ch",
EmailVerified: true,
Phone: "",
PhoneVerified: false,
PreferredLanguage: language.German,
IsAdmin: false,
},
},
}
}
// ExampleClientID is only used in the example server
func (u userStore) ExampleClientID() string {
return "service"
}
func (u userStore) GetUserByID(id string) *User {
return u.users[id]
}
func (u userStore) GetUserByUsername(username string) *User {
for _, user := range u.users {
if user.Username == username {
return user
}
}
return nil
}
golang-github-zitadel-oidc-3.27.0/go.mod 0000664 0000000 0000000 00000002247 14656014552 0020023 0 ustar 00root root 0000000 0000000 module github.com/zitadel/oidc/v3
go 1.21
require (
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/go-chi/chi/v5 v5.1.0
github.com/go-jose/go-jose/v4 v4.0.4
github.com/golang/mock v1.6.0
github.com/google/go-github/v31 v31.0.0
github.com/google/uuid v1.6.0
github.com/gorilla/securecookie v1.1.2
github.com/jeremija/gosubmit v0.2.7
github.com/muhlemmer/gu v0.3.1
github.com/muhlemmer/httpforwarded v0.1.0
github.com/rs/cors v1.11.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
github.com/zitadel/logging v0.6.0
github.com/zitadel/schema v1.3.0
go.opentelemetry.io/otel v1.28.0
golang.org/x/oauth2 v0.22.0
golang.org/x/text v0.16.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.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
golang-github-zitadel-oidc-3.27.0/go.sum 0000664 0000000 0000000 00000023021 14656014552 0020041 0 ustar 00root root 0000000 0000000 github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
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/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
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/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank=
github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20190620200207-3b0461eec859/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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/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-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
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-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
golang-github-zitadel-oidc-3.27.0/internal/ 0000775 0000000 0000000 00000000000 14656014552 0020524 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/internal/testutil/ 0000775 0000000 0000000 00000000000 14656014552 0022401 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/internal/testutil/gen/ 0000775 0000000 0000000 00000000000 14656014552 0023152 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/internal/testutil/gen/gen.go 0000664 0000000 0000000 00000002542 14656014552 0024255 0 ustar 00root root 0000000 0000000 // Package gen allows generating of example tokens and claims.
//
// go run ./internal/testutil/gen
package main
import (
"encoding/json"
"fmt"
"os"
tu "github.com/zitadel/oidc/v3/internal/testutil"
"github.com/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)
}
golang-github-zitadel-oidc-3.27.0/internal/testutil/token.go 0000664 0000000 0000000 00000017651 14656014552 0024062 0 ustar 00root root 0000000 0000000 // Package testuril helps setting up required data for testing,
// such as tokens, claims and verifiers.
package testutil
import (
"context"
"encoding/json"
"errors"
"time"
jose "github.com/go-jose/go-jose/v4"
"github.com/muhlemmer/gu"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
// 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
}
golang-github-zitadel-oidc-3.27.0/pkg/ 0000775 0000000 0000000 00000000000 14656014552 0017471 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/client/ 0000775 0000000 0000000 00000000000 14656014552 0020747 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/client/client.go 0000664 0000000 0000000 00000021440 14656014552 0022555 0 ustar 00root root 0000000 0000000 package client
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/logging"
"go.opentelemetry.io/otel"
"golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/pkg/crypto"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
var (
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
// 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) {
ctx, span := Tracer.Start(ctx, "Discover")
defer span.End()
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" {
wellKnown = wellKnownUrl[0]
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, wellKnown, nil)
if err != nil {
return nil, err
}
discoveryConfig := new(oidc.DiscoveryConfiguration)
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
if err != nil {
return nil, err
}
if logger, ok := logging.FromContext(ctx); ok {
logger.Debug("discover", "config", discoveryConfig)
}
if discoveryConfig.Issuer != issuer {
return nil, oidc.ErrIssuerInvalid
}
return discoveryConfig, nil
}
type TokenEndpointCaller interface {
TokenEndpoint() string
HttpClient() *http.Client
}
func CallTokenEndpoint(ctx context.Context, request any, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) {
return callTokenEndpoint(ctx, request, nil, caller)
}
func callTokenEndpoint(ctx context.Context, request any, authFn any, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) {
ctx, span := Tracer.Start(ctx, "callTokenEndpoint")
defer span.End()
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn)
if err != nil {
return nil, err
}
tokenRes := new(oidc.AccessTokenResponse)
if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil {
return nil, err
}
token := &oauth2.Token{
AccessToken: tokenRes.AccessToken,
TokenType: tokenRes.TokenType,
RefreshToken: tokenRes.RefreshToken,
Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second),
}
if tokenRes.IDToken != "" {
token = token.WithExtra(map[string]any{
"id_token": tokenRes.IDToken,
})
}
return token, nil
}
type EndSessionCaller interface {
GetEndSessionEndpoint() string
HttpClient() *http.Client
}
func CallEndSessionEndpoint(ctx context.Context, request any, authFn any, caller EndSessionCaller) (*url.URL, error) {
ctx, span := Tracer.Start(ctx, "CallEndSessionEndpoint")
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 {
return nil, err
}
client := caller.HttpClient()
client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("EndSession failure, %d status code: %s", resp.StatusCode, string(body))
}
location, err := resp.Location()
if err != nil {
if errors.Is(err, http.ErrNoLocation) {
return nil, nil
}
return nil, err
}
return location, nil
}
type RevokeCaller interface {
GetRevokeEndpoint() string
HttpClient() *http.Client
}
type RevokeRequest struct {
Token string `schema:"token"`
TokenTypeHint string `schema:"token_type_hint"`
ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"`
}
func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller RevokeCaller) error {
ctx, span := Tracer.Start(ctx, "CallRevokeEndpoint")
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 {
return err
}
client := caller.HttpClient()
client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// According to RFC7009 in section 2.2:
// "The content of the response body is ignored by the client as all
// necessary information is conveyed in the response code."
if resp.StatusCode != 200 {
body, err := io.ReadAll(resp.Body)
if err == nil {
return fmt.Errorf("revoke returned status %d and text: %s", resp.StatusCode, string(body))
} else {
return fmt.Errorf("revoke returned status %d", resp.StatusCode)
}
}
return nil
}
func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) {
ctx, span := Tracer.Start(ctx, "CallTokenExchangeEndpoint")
defer span.End()
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn)
if err != nil {
return nil, err
}
tokenRes := new(oidc.TokenExchangeResponse)
if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil {
return nil, err
}
return tokenRes, nil
}
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
privateKey, algorithm, err := crypto.BytesToPrivateKey(key)
if err != nil {
return nil, err
}
signingKey := jose.SigningKey{
Algorithm: algorithm,
Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID},
}
return jose.NewSigner(signingKey, &jose.SignerOptions{})
}
func SignedJWTProfileAssertion(clientID string, audience []string, expiration time.Duration, signer jose.Signer) (string, error) {
iat := time.Now()
exp := iat.Add(expiration)
return crypto.Sign(&oidc.JWTTokenRequest{
Issuer: clientID,
Subject: clientID,
Audience: audience,
ExpiresAt: oidc.FromTime(exp),
IssuedAt: oidc.FromTime(iat),
}, signer)
}
type DeviceAuthorizationCaller interface {
GetDeviceAuthorizationEndpoint() string
HttpClient() *http.Client
}
func CallDeviceAuthorizationEndpoint(ctx context.Context, request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller, authFn any) (*oidc.DeviceAuthorizationResponse, error) {
ctx, span := Tracer.Start(ctx, "CallDeviceAuthorizationEndpoint")
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 {
return nil, err
}
if request.ClientSecret != "" {
req.SetBasicAuth(request.ClientID, request.ClientSecret)
}
resp := new(oidc.DeviceAuthorizationResponse)
if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil {
return nil, err
}
return resp, nil
}
type DeviceAccessTokenRequest struct {
*oidc.ClientCredentialsRequest
oidc.DeviceAccessTokenRequest
}
func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
ctx, span := Tracer.Start(ctx, "CallDeviceAccessTokenEndpoint")
defer span.End()
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, nil)
if err != nil {
return nil, err
}
if request.ClientSecret != "" {
req.SetBasicAuth(request.ClientID, request.ClientSecret)
}
resp := new(oidc.AccessTokenResponse)
if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil {
return nil, err
}
return resp, nil
}
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 {
timer := time.After(interval)
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-timer:
}
ctx, cancel := context.WithTimeout(ctx, interval)
defer cancel()
resp, err := CallDeviceAccessTokenEndpoint(ctx, request, caller)
if err == nil {
return resp, nil
}
if errors.Is(err, context.DeadlineExceeded) {
interval += 5 * time.Second
}
var target *oidc.Error
if !errors.As(err, &target) {
return nil, err
}
switch target.ErrorType {
case oidc.AuthorizationPending:
continue
case oidc.SlowDown:
interval += 5 * time.Second
continue
default:
return nil, err
}
}
}
golang-github-zitadel-oidc-3.27.0/pkg/client/client_test.go 0000664 0000000 0000000 00000002076 14656014552 0023620 0 ustar 00root root 0000000 0000000 package client
import (
"context"
"net/http"
"testing"
"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 bool
}{
{
name: "spotify", // https://github.com/zitadel/oidc/issues/406
args: args{
issuer: "https://accounts.spotify.com",
},
wantFields: &wantFields{
UILocalesSupported: true,
},
wantErr: false,
},
}
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...)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
if tt.wantFields == nil {
return
}
assert.Equal(t, tt.args.issuer, got.Issuer)
if tt.wantFields.UILocalesSupported {
assert.NotEmpty(t, got.UILocalesSupported)
}
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/client/errors.go 0000664 0000000 0000000 00000000130 14656014552 0022604 0 ustar 00root root 0000000 0000000 package client
import "errors"
var ErrEndpointNotSet = errors.New("endpoint not set")
golang-github-zitadel-oidc-3.27.0/pkg/client/integration_test.go 0000664 0000000 0000000 00000047232 14656014552 0024670 0 ustar 00root root 0000000 0000000 package client_test
import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"math/rand"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"os/signal"
"strconv"
"syscall"
"testing"
"time"
"github.com/jeremija/gosubmit"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/example/server/exampleop"
"github.com/zitadel/oidc/v3/example/server/storage"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/client/rs"
"github.com/zitadel/oidc/v3/pkg/client/tokenexchange"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/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) {
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 ------")
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, wrapServer)
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
t.Log("------- run authorization code flow ------")
provider, tokens := RunAuthorizationCodeFlow(t, opServer, clientID, "secret")
t.Log("------- refresh tokens ------")
newTokens, err := rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "")
require.NoError(t, err, "refresh token")
assert.NotNil(t, newTokens, "access token")
t.Logf("new access token %s", newTokens.AccessToken)
t.Logf("new refresh token %s", newTokens.RefreshToken)
t.Logf("new token type %s", newTokens.TokenType)
t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339))
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) ------")
newLoc, err := rp.EndSession(CTX, provider, tokens.IDToken, "", "")
require.NoError(t, err, "logout")
if newLoc != nil {
t.Logf("redirect to %s", newLoc)
} else {
t.Logf("no redirect")
}
t.Log("------ attempt refresh again (should fail) ------")
t.Log("trying original refresh token", tokens.RefreshToken)
_, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "")
assert.Errorf(t, err, "refresh with original")
if newTokens.RefreshToken != "" {
t.Log("trying replacement refresh token", newTokens.RefreshToken)
_, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, newTokens.RefreshToken, "", "")
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) {
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 ------")
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, wrapServer)
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
clientSecret := "secret"
t.Log("------- run authorization code flow ------")
provider, tokens := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret)
resourceServer, err := rs.NewResourceServerClientCredentials(CTX, opServer.URL, clientID, clientSecret)
require.NoError(t, err, "new resource server")
t.Log("------- exchage refresh tokens (impersonation) ------")
tokenExchangeResponse, err := tokenexchange.ExchangeToken(
CTX,
resourceServer,
tokens.RefreshToken,
oidc.RefreshTokenType,
"",
"",
[]string{},
[]string{},
[]string{"profile", "custom_scope:impersonate:id2"},
oidc.RefreshTokenType,
)
require.NoError(t, err, "refresh token")
require.NotNil(t, tokenExchangeResponse, "token exchange response")
assert.Equal(t, tokenExchangeResponse.IssuedTokenType, oidc.RefreshTokenType)
assert.NotEmpty(t, tokenExchangeResponse.AccessToken, "access token")
assert.NotEmpty(t, tokenExchangeResponse.RefreshToken, "refresh token")
assert.Equal(t, []string(tokenExchangeResponse.Scopes), []string{"profile", "custom_scope:impersonate:id2"})
t.Log("------ end session (logout) ------")
newLoc, err := rp.EndSession(CTX, provider, tokens.IDToken, "", "")
require.NoError(t, err, "logout")
if newLoc != nil {
t.Logf("redirect to %s", newLoc)
} else {
t.Logf("no redirect")
}
t.Log("------- attempt exchage again (should fail) ------")
tokenExchangeResponse, err = tokenexchange.ExchangeToken(
CTX,
resourceServer,
tokens.RefreshToken,
oidc.RefreshTokenType,
"",
"",
[]string{},
[]string{},
[]string{"profile", "custom_scope:impersonate:id2"},
oidc.RefreshTokenType,
)
require.Error(t, err, "refresh token")
assert.Contains(t, err.Error(), "subject_token is invalid")
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]) {
targetURL := "http://local-site"
localURL, err := url.Parse(targetURL + "/login?requestID=1234")
require.NoError(t, err, "local url")
client := storage.WebClient(clientID, clientSecret, targetURL)
storage.RegisterClients(client)
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("------- 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.WithAuthStyle(oauth2.AuthStyleInHeader),
rp.WithVerifierOpts(
rp.WithIssuedAtOffset(5*time.Second),
rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"),
),
)
require.NoError(t, err, "new rp")
t.Log("------- get redirect from local client (rp) to OP ------")
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
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())
}
}()
require.GreaterOrEqual(t, capturedW.Code, 200, "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
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 to OP to login page ------")
loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL)
t.Log("login page URL", loginPageURL)
t.Log("------- get login form ------")
form := getForm(t, "get login form", httpClient, loginPageURL)
t.Log("login form (unfilled)", string(form))
defer func() {
if t.Failed() {
t.Logf("login form (unfilled): %s", string(form))
}
}()
t.Log("------- post to login form, get redirect to OP ------")
postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL,
gosubmit.Set("username", "test-user@local-site"),
gosubmit.Set("password", "verysecure"))
t.Logf("Get redirect from %s", postLoginRedirectURL)
t.Log("------- redirect from OP back to RP ------")
codeBearingURL := getRedirect(t, "get redirect with code", httpClient, postLoginRedirectURL)
t.Logf("Redirect with code %s", codeBearingURL)
t.Log("------- exchange code for tokens ------")
capturedW = httptest.NewRecorder()
get = httptest.NewRequest("GET", codeBearingURL.String(), nil)
for _, cookie := range jar.Cookies(codeBearingURL) {
get.Header["Cookie"] = append(get.Header["Cookie"], cookie.String())
t.Logf("setting cookie %s", cookie)
}
var email string
redirect := func(w http.ResponseWriter, r *http.Request, newTokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
tokens = newTokens
require.NotNil(t, tokens, "tokens")
require.NotNil(t, info, "info")
t.Log("access token", tokens.AccessToken)
t.Log("refresh token", tokens.RefreshToken)
t.Log("id token", tokens.IDToken)
t.Log("email", info.Email)
email = info.Email
http.Redirect(w, r, targetURL, 302)
}
rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider, rp.WithURLParam("custom", "param"))(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")
}
}()
require.Less(t, capturedW.Code, 400, "token exchange response code")
// TODO: how to check the custom header was sent to the server?
//nolint:bodyclose
resp = capturedW.Result()
authorizedURL, err := resp.Location()
require.NoError(t, err, "get fully-authorizied redirect location")
require.Equal(t, targetURL, authorizedURL.String(), "fully-authorizied redirect location")
require.NotEmpty(t, tokens.IDToken, "id token")
assert.NotEmpty(t, tokens.RefreshToken, "refresh token")
assert.NotEmpty(t, tokens.AccessToken, "access token")
assert.NotEmpty(t, email, "email")
return provider, tokens
}
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 {
http.Handler
}
func getRedirect(t *testing.T, desc string, httpClient *http.Client, uri *url.URL) *url.URL {
req := &http.Request{
Method: "GET",
URL: uri,
Header: make(http.Header),
}
resp, err := httpClient.Do(req)
require.NoError(t, err, "GET "+uri.String())
defer func() {
if t.Failed() {
body, _ := io.ReadAll(resp.Body)
t.Logf("%s: GET %s: body: %s", desc, uri, string(body))
}
}()
//nolint:errcheck
defer resp.Body.Close()
redirect, err := resp.Location()
require.NoErrorf(t, err, "%s: get redirect %s", desc, uri)
require.NotEmptyf(t, redirect, "%s: get redirect %s", desc, uri)
return redirect
}
func getForm(t *testing.T, desc string, httpClient *http.Client, uri *url.URL) []byte {
req := &http.Request{
Method: "GET",
URL: uri,
Header: make(http.Header),
}
resp, err := httpClient.Do(req)
require.NoErrorf(t, err, "%s: GET %s", desc, uri)
//nolint:errcheck
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "%s: read GET %s", desc, uri)
return body
}
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
req := gosubmit.ParseWithURL(io.NopCloser(bytes.NewReader(body)), uri.String()).FirstForm().Testing(t).NewTestRequest(
append([]gosubmit.Option{gosubmit.AutoFill()}, opts...)...,
)
if req.URL.Scheme == "" {
req.URL = uri
t.Log("request lost it's proto..., adding back... request now", req.URL)
}
req.RequestURI = "" // bug in gosubmit?
resp, err := httpClient.Do(req)
require.NoErrorf(t, err, "%s: POST %s", desc, uri)
//nolint:errcheck
defer resp.Body.Close()
defer func() {
if t.Failed() {
body, _ := io.ReadAll(resp.Body)
t.Logf("%s: GET %s: body: %s", desc, uri, string(body))
}
}()
redirect, err := resp.Location()
require.NoErrorf(t, err, "%s: redirect for POST %s", desc, uri)
return redirect
}
golang-github-zitadel-oidc-3.27.0/pkg/client/jwt_profile.go 0000664 0000000 0000000 00000001645 14656014552 0023630 0 ustar 00root root 0000000 0000000 package client
import (
"context"
"net/url"
"golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
// JWTProfileExchange handles the oauth2 jwt profile exchange
func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller TokenEndpointCaller) (*oauth2.Token, error) {
return CallTokenEndpoint(ctx, jwtProfileGrantRequest, caller)
}
func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("client_assertion", assertion),
oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion),
}
}
func ClientAssertionFormAuthorization(assertion string) http.FormAuthorization {
return func(values url.Values) {
values.Set("client_assertion", assertion)
values.Set("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion)
}
}
golang-github-zitadel-oidc-3.27.0/pkg/client/key.go 0000664 0000000 0000000 00000001405 14656014552 0022066 0 ustar 00root root 0000000 0000000 package client
import (
"encoding/json"
"io/ioutil"
)
const (
serviceAccountKey = "serviceaccount"
applicationKey = "application"
)
type KeyFile struct {
Type string `json:"type"` // serviceaccount or application
KeyID string `json:"keyId"`
Key string `json:"key"`
Issuer string `json:"issuer"` // not yet in file
// serviceaccount
UserID string `json:"userId"`
// application
ClientID string `json:"clientId"`
}
func ConfigFromKeyFile(path string) (*KeyFile, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return ConfigFromKeyFileData(data)
}
func ConfigFromKeyFileData(data []byte) (*KeyFile, error) {
var f KeyFile
if err := json.Unmarshal(data, &f); err != nil {
return nil, err
}
return &f, nil
}
golang-github-zitadel-oidc-3.27.0/pkg/client/profile/ 0000775 0000000 0000000 00000000000 14656014552 0022407 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/client/profile/jwt_profile.go 0000664 0000000 0000000 00000007763 14656014552 0025277 0 ustar 00root root 0000000 0000000 package profile
import (
"context"
"net/http"
"time"
jose "github.com/go-jose/go-jose/v4"
"golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
type TokenSource interface {
oauth2.TokenSource
TokenCtx(context.Context) (*oauth2.Token, error)
}
// jwtProfileTokenSource implement the oauth2.TokenSource
// it will request a token using the OAuth2 JWT Profile Grant
// therefore sending an `assertion` by signing a JWT with the provided private key
type jwtProfileTokenSource struct {
clientID string
audience []string
signer jose.Signer
scopes []string
httpClient *http.Client
tokenEndpoint string
}
// NewJWTProfileTokenSourceFromKeyFile returns an implementation of TokenSource
// It will request a token using the OAuth2 JWT Profile Grant,
// 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 {
return nil, err
}
return NewJWTProfileTokenSource(ctx, issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
}
// NewJWTProfileTokenSourceFromKeyFileData returns an implementation of oauth2.TokenSource
// It will request a token using the OAuth2 JWT Profile Grant,
// 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 {
return nil, err
}
return NewJWTProfileTokenSource(ctx, issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
}
// NewJWTProfileSource returns an implementation of oauth2.TokenSource
// 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)
if err != nil {
return nil, err
}
source := &jwtProfileTokenSource{
clientID: clientID,
audience: []string{issuer},
signer: signer,
scopes: scopes,
httpClient: http.DefaultClient,
}
for _, opt := range options {
opt(source)
}
if source.tokenEndpoint == "" {
config, err := client.Discover(ctx, issuer, source.httpClient)
if err != nil {
return nil, err
}
source.tokenEndpoint = config.TokenEndpoint
}
return source, nil
}
func WithHTTPClient(client *http.Client) func(source *jwtProfileTokenSource) {
return func(source *jwtProfileTokenSource) {
source.httpClient = client
}
}
func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(source *jwtProfileTokenSource) {
return func(source *jwtProfileTokenSource) {
source.tokenEndpoint = tokenEndpoint
}
}
func (j *jwtProfileTokenSource) TokenEndpoint() string {
return j.tokenEndpoint
}
func (j *jwtProfileTokenSource) HttpClient() *http.Client {
return j.httpClient
}
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)
if err != nil {
return nil, err
}
return client.JWTProfileExchange(ctx, oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j)
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/ 0000775 0000000 0000000 00000000000 14656014552 0021370 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/client/rp/cli/ 0000775 0000000 0000000 00000000000 14656014552 0022137 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/client/rp/cli/browser.go 0000664 0000000 0000000 00000000667 14656014552 0024162 0 ustar 00root root 0000000 0000000 package cli
import (
"fmt"
"log"
"os/exec"
"runtime"
)
func OpenBrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Fatal(err)
}
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/cli/cli.go 0000664 0000000 0000000 00000002037 14656014552 0023237 0 ustar 00root root 0000000 0000000 package cli
import (
"context"
"net/http"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
const (
loginPath = "/login"
)
func CodeFlow[C oidc.IDClaims](ctx context.Context, relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens[C] {
codeflowCtx, codeflowCancel := context.WithCancel(ctx)
defer codeflowCancel()
tokenChan := make(chan *oidc.Tokens[C], 1)
callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp rp.RelyingParty) {
tokenChan <- tokens
msg := "
Success!
"
msg = msg + "
You are authenticated and can now return to the CLI.
"
w.Write([]byte(msg))
}
http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relyingParty))
http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relyingParty))
httphelper.StartServer(codeflowCtx, ":"+port)
OpenBrowser("http://localhost:" + port + loginPath)
return <-tokenChan
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/delegation.go 0000664 0000000 0000000 00000001141 14656014552 0024027 0 ustar 00root root 0000000 0000000 package rp
import (
"github.com/zitadel/oidc/v3/pkg/oidc/grants/tokenexchange"
)
// DelegationTokenRequest is an implementation of TokenExchangeRequest
// it exchanges an "urn:ietf:params:oauth:token-type:access_token" with an optional
// "urn:ietf:params:oauth:token-type:access_token" actor token for an
// "urn:ietf:params:oauth:token-type:access_token" delegation token
func DelegationTokenRequest(subjectToken string, opts ...tokenexchange.TokenExchangeOption) *tokenexchange.TokenExchangeRequest {
return tokenexchange.NewTokenExchangeRequest(subjectToken, tokenexchange.AccessTokenType, opts...)
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/device.go 0000664 0000000 0000000 00000004350 14656014552 0023160 0 ustar 00root root 0000000 0000000 package rp
import (
"context"
"fmt"
"time"
"github.com/zitadel/oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) {
confg := rp.OAuthConfig()
req := &oidc.ClientCredentialsRequest{
Scope: scopes,
ClientID: confg.ClientID,
ClientSecret: confg.ClientSecret,
}
if signer := rp.Signer(); signer != nil {
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, signer)
if err != nil {
return nil, fmt.Errorf("failed to build assertion: %w", err)
}
req.ClientAssertion = assertion
req.ClientAssertionType = oidc.ClientAssertionTypeJWTAssertion
}
return req, nil
}
// DeviceAuthorization starts a new Device Authorization flow as defined
// in RFC 8628, section 3.1 and 3.2:
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1
func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, authFn any) (*oidc.DeviceAuthorizationResponse, error) {
ctx, span := client.Tracer.Start(ctx, "DeviceAuthorization")
defer span.End()
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAuthorization")
req, err := newDeviceClientCredentialsRequest(scopes, rp)
if err != nil {
return nil, err
}
return client.CallDeviceAuthorizationEndpoint(ctx, req, rp, authFn)
}
// DeviceAccessToken attempts to obtain tokens from a Device Authorization,
// by means of polling as defined in RFC, section 3.3 and 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) {
ctx, span := client.Tracer.Start(ctx, "DeviceAccessToken")
defer span.End()
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAccessToken")
req := &client.DeviceAccessTokenRequest{
DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{
GrantType: oidc.GrantTypeDeviceCode,
DeviceCode: deviceCode,
},
}
req.ClientCredentialsRequest, err = newDeviceClientCredentialsRequest(nil, rp)
if err != nil {
return nil, err
}
return client.PollDeviceAccessTokenEndpoint(ctx, interval, req, tokenEndpointCaller{rp})
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/errors.go 0000664 0000000 0000000 00000000202 14656014552 0023225 0 ustar 00root root 0000000 0000000 package rp
import "errors"
var ErrRelyingPartyNotSupportRevokeCaller = errors.New("RelyingParty does not support RevokeCaller")
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/jwks.go 0000664 0000000 0000000 00000016120 14656014552 0022675 0 ustar 00root root 0000000 0000000 package rp
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sync"
jose "github.com/go-jose/go-jose/v4"
"github.com/zitadel/oidc/v3/pkg/client"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet {
keyset := &remoteKeySet{httpClient: client, jwksURL: jwksURL}
for _, opt := range opts {
opt(keyset)
}
return keyset
}
// SkipRemoteCheck will suppress checking for new remote keys if signature validation fails with cached keys
// and no kid header is set in the JWT
//
// this might be handy to save some unnecessary round trips in cases where the JWT does not contain a kid header and
// there is only a single remote key
// please notice that remote keys will then only be fetched if cached keys are empty
func SkipRemoteCheck() func(set *remoteKeySet) {
return func(set *remoteKeySet) {
set.skipRemoteCheck = true
}
}
type remoteKeySet struct {
jwksURL string
httpClient *http.Client
defaultAlg string
skipRemoteCheck bool
// guard all other fields
mu sync.Mutex
// inflight suppresses parallel execution of updateKeys and allows
// multiple goroutines to wait for its result.
inflight *inflight
// A set of cached keys and their expiry.
cachedKeys []jose.JSONWebKey
}
// inflight is used to wait on some in-flight request from multiple goroutines.
type inflight struct {
doneCh chan struct{}
keys []jose.JSONWebKey
err error
}
func newInflight() *inflight {
return &inflight{doneCh: make(chan struct{})}
}
// wait returns a channel that multiple goroutines can receive on. Once it returns
// a value, the inflight request is done and result() can be inspected.
func (i *inflight) wait() <-chan struct{} {
return i.doneCh
}
// done can only be called by a single goroutine. It records the result of the
// inflight request and signals other goroutines that the result is safe to
// inspect.
func (i *inflight) done(keys []jose.JSONWebKey, err error) {
i.keys = keys
i.err = err
close(i.doneCh)
}
// result cannot be called until the wait() channel has returned a value.
func (i *inflight) result() ([]jose.JSONWebKey, error) {
return i.keys, i.err
}
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)
if alg == "" {
alg = r.defaultAlg
}
payload, err := r.verifySignatureCached(jws, keyID, alg)
if payload != nil {
return payload, nil
}
if err != nil {
return nil, err
}
return r.verifySignatureRemote(ctx, jws, keyID, alg)
}
// verifySignatureCached checks for a matching key in the cached key list
//
// if there is only one possible, it tries to verify the signature and will return the payload if successful
//
// it only returns an error if signature validation fails and keys exactMatch which is if either:
// - both kid are empty and skipRemoteCheck is set to true
// - or both (JWT and JWK) kid are equal
//
// otherwise it will return no error (so remote keys will be loaded)
func (r *remoteKeySet) verifySignatureCached(jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) {
keys := r.keysFromCache()
if len(keys) == 0 {
return nil, nil
}
key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keys...)
if err != nil {
// no key / multiple found, try with remote keys
return nil, nil //nolint:nilerr
}
payload, err := jws.Verify(&key)
if payload != nil {
return payload, nil
}
if !r.exactMatch(key.KeyID, keyID) {
// no exact key match, try getting better match with remote keys
return nil, nil
}
return nil, fmt.Errorf("signature verification failed: %w", err)
}
func (r *remoteKeySet) exactMatch(jwkID, jwsID string) bool {
if jwkID == "" && jwsID == "" {
return r.skipRemoteCheck
}
return jwkID == jwsID
}
func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) {
ctx, span := client.Tracer.Start(ctx, "verifySignatureRemote")
defer span.End()
keys, err := r.keysFromRemote(ctx)
if err != nil {
return nil, fmt.Errorf("unable to fetch key for signature validation: %w", err)
}
key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keys...)
if err != nil {
return nil, fmt.Errorf("unable to validate signature: %w", err)
}
payload, err := jws.Verify(&key)
if err != nil {
return nil, fmt.Errorf("signature verification failed: %w", err)
}
return payload, nil
}
func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey) {
r.mu.Lock()
defer r.mu.Unlock()
return r.cachedKeys
}
// keysFromRemote syncs the key set from the remote set, records the values in the
// cache, and returns the key set.
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.
r.mu.Lock()
// If there's not a current inflight request, create one.
if r.inflight == nil {
r.inflight = newInflight()
// This goroutine has exclusive ownership over the current inflight
// request. It releases the resource by nil'ing the inflight field
// once the goroutine is done.
go r.updateKeys(ctx)
}
inflight := r.inflight
r.mu.Unlock()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-inflight.wait():
return inflight.result()
}
}
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.
keys, err := r.fetchRemoteKeys(ctx)
r.inflight.done(keys, err)
// Lock to update the keys and indicate that there is no longer an
// inflight request.
r.mu.Lock()
defer r.mu.Unlock()
if err == nil {
r.cachedKeys = keys
}
// Free inflight so a different request can run.
r.inflight = nil
}
func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, error) {
ctx, span := client.Tracer.Start(ctx, "fetchRemoteKeys")
defer span.End()
req, err := http.NewRequestWithContext(ctx, "GET", r.jwksURL, nil)
if err != nil {
return nil, fmt.Errorf("oidc: can't create request: %v", err)
}
keySet := new(jsonWebKeySet)
if err = httphelper.HttpRequest(r.httpClient, req, keySet); err != nil {
return nil, fmt.Errorf("oidc: failed to get keys: %v", err)
}
return keySet.Keys, nil
}
// jsonWebKeySet is an alias for jose.JSONWebKeySet which ignores unknown key types (kty)
type jsonWebKeySet jose.JSONWebKeySet
// UnmarshalJSON overrides the default jose.JSONWebKeySet method to ignore any error
// which might occur because of unknown key types (kty)
func (k *jsonWebKeySet) UnmarshalJSON(data []byte) (err error) {
var raw rawJSONWebKeySet
err = json.Unmarshal(data, &raw)
if err != nil {
return err
}
for _, key := range raw.Keys {
webKey := new(jose.JSONWebKey)
err = webKey.UnmarshalJSON(key)
if err == nil {
k.Keys = append(k.Keys, *webKey)
}
}
return nil
}
type rawJSONWebKeySet struct {
Keys []json.RawMessage `json:"keys"`
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/log.go 0000664 0000000 0000000 00000000475 14656014552 0022506 0 ustar 00root root 0000000 0000000 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)
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/relying_party.go 0000664 0000000 0000000 00000066710 14656014552 0024621 0 ustar 00root root 0000000 0000000 package rp
import (
"context"
"encoding/base64"
"errors"
"log/slog"
"net/http"
"net/url"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/google/uuid"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/client"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
const (
idTokenKey = "id_token"
stateParam = "state"
pkceCode = "pkce"
)
var ErrUserInfoSubNotMatching = errors.New("sub from userinfo does not match the sub from the id_token")
// RelyingParty declares the minimal interface for oidc clients
type RelyingParty interface {
// OAuthConfig returns the oauth2 Config
OAuthConfig() *oauth2.Config
// Issuer returns the issuer of the oidc config
Issuer() string
// IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)`
IsPKCE() bool
// CookieHandler returns a http cookie handler used for various state transfer cookies
CookieHandler() *httphelper.CookieHandler
// HttpClient returns a http client used for calls to the openid provider, e.g. calling token endpoint
HttpClient() *http.Client
// IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls
IsOAuth2Only() bool
// Signer is used if the relaying party uses the JWT Profile
Signer() jose.Signer
// GetEndSessionEndpoint returns the endpoint to sign out on a IDP
GetEndSessionEndpoint() string
// GetRevokeEndpoint returns the endpoint to revoke a specific token
GetRevokeEndpoint() string
// UserinfoEndpoint returns the userinfo
UserinfoEndpoint() string
// GetDeviceAuthorizationEndpoint returns the endpoint which can
// be used to start a DeviceAuthorization flow.
GetDeviceAuthorizationEndpoint() string
// IDTokenVerifier returns the verifier used for oidc id_token verification
IDTokenVerifier() *IDTokenVerifier
// ErrorHandler returns the handler used for callback errors
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 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) {
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 {
issuer string
DiscoveryEndpoint string
endpoints Endpoints
oauthConfig *oauth2.Config
oauth2Only bool
pkce bool
useSigningAlgsFromDiscovery bool
httpClient *http.Client
cookieHandler *httphelper.CookieHandler
oauthAuthStyle oauth2.AuthStyle
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
unauthorizedHandler func(http.ResponseWriter, *http.Request, string, string)
idTokenVerifier *IDTokenVerifier
verifierOpts []VerifierOption
signer jose.Signer
logger *slog.Logger
}
func (rp *relyingParty) OAuthConfig() *oauth2.Config {
return rp.oauthConfig
}
func (rp *relyingParty) Issuer() string {
return rp.issuer
}
func (rp *relyingParty) IsPKCE() bool {
return rp.pkce
}
func (rp *relyingParty) CookieHandler() *httphelper.CookieHandler {
return rp.cookieHandler
}
func (rp *relyingParty) HttpClient() *http.Client {
return rp.httpClient
}
func (rp *relyingParty) IsOAuth2Only() bool {
return rp.oauth2Only
}
func (rp *relyingParty) Signer() jose.Signer {
return rp.signer
}
func (rp *relyingParty) UserinfoEndpoint() string {
return rp.endpoints.UserinfoURL
}
func (rp *relyingParty) GetDeviceAuthorizationEndpoint() string {
return rp.endpoints.DeviceAuthorizationURL
}
func (rp *relyingParty) GetEndSessionEndpoint() string {
return rp.endpoints.EndSessionURL
}
func (rp *relyingParty) GetRevokeEndpoint() string {
return rp.endpoints.RevokeURL
}
func (rp *relyingParty) IDTokenVerifier() *IDTokenVerifier {
if rp.idTokenVerifier == nil {
rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...)
}
return rp.idTokenVerifier
}
func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) {
if rp.errorHandler == nil {
rp.errorHandler = DefaultErrorHandler
}
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
// OAuth2 Config and possible configOptions
// it will use the AuthURL and TokenURL set in config
func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) {
rp := &relyingParty{
oauthConfig: config,
httpClient: httphelper.DefaultHTTPClient,
oauth2Only: true,
unauthorizedHandler: DefaultUnauthorizedHandler,
oauthAuthStyle: oauth2.AuthStyleAutoDetect,
}
for _, optFunc := range options {
if err := optFunc(rp); err != nil {
return nil, err
}
}
rp.oauthConfig.Endpoint.AuthStyle = rp.oauthAuthStyle
// avoid races by calling these early
_ = rp.IDTokenVerifier() // sets idTokenVerifier
_ = rp.ErrorHandler() // sets errorHandler
_ = rp.UnauthorizedHandler() // sets unauthorizedHandler
return rp, nil
}
// NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given
// issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions
// 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) {
rp := &relyingParty{
issuer: issuer,
oauthConfig: &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURI,
Scopes: scopes,
},
httpClient: httphelper.DefaultHTTPClient,
oauth2Only: false,
oauthAuthStyle: oauth2.AuthStyleAutoDetect,
}
for _, optFunc := range options {
if err := optFunc(rp); err != nil {
return nil, err
}
}
ctx = logCtxWithRPData(ctx, rp, "function", "NewRelyingPartyOIDC")
discoveryConfiguration, err := client.Discover(ctx, rp.issuer, rp.httpClient, rp.DiscoveryEndpoint)
if err != nil {
return nil, err
}
if rp.useSigningAlgsFromDiscovery {
rp.verifierOpts = append(rp.verifierOpts, WithSupportedSigningAlgorithms(discoveryConfiguration.IDTokenSigningAlgValuesSupported...))
}
endpoints := GetEndpoints(discoveryConfiguration)
rp.oauthConfig.Endpoint = endpoints.Endpoint
rp.endpoints = endpoints
rp.oauthConfig.Endpoint.AuthStyle = rp.oauthAuthStyle
rp.endpoints.Endpoint.AuthStyle = rp.oauthAuthStyle
// avoid races by calling these early
_ = rp.IDTokenVerifier() // sets idTokenVerifier
_ = rp.ErrorHandler() // sets errorHandler
_ = rp.UnauthorizedHandler() // sets unauthorizedHandler
return rp, nil
}
// Option is the type for providing dynamic options to the relyingParty
type Option func(*relyingParty) error
func WithCustomDiscoveryUrl(url string) Option {
return func(rp *relyingParty) error {
rp.DiscoveryEndpoint = url
return nil
}
}
// WithCookieHandler set a `CookieHandler` for securing the various redirects
func WithCookieHandler(cookieHandler *httphelper.CookieHandler) Option {
return func(rp *relyingParty) error {
rp.cookieHandler = cookieHandler
return nil
}
}
// WithPKCE sets the RP to use PKCE (oauth2 code challenge)
// it also sets a `CookieHandler` for securing the various redirects
// and exchanging the code challenge
func WithPKCE(cookieHandler *httphelper.CookieHandler) Option {
return func(rp *relyingParty) error {
rp.pkce = true
rp.cookieHandler = cookieHandler
return nil
}
}
// WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier
func WithHTTPClient(client *http.Client) Option {
return func(rp *relyingParty) error {
rp.httpClient = client
return nil
}
}
func WithErrorHandler(errorHandler ErrorHandler) Option {
return func(rp *relyingParty) error {
rp.errorHandler = errorHandler
return nil
}
}
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 {
return func(rp *relyingParty) error {
rp.verifierOpts = opts
return nil
}
}
// WithClientKey specifies the path to the key.json to be used for the JWT Profile Client Authentication on the token endpoint
//
// deprecated: use WithJWTProfile(SignerFromKeyPath(path)) instead
func WithClientKey(path string) Option {
return WithJWTProfile(SignerFromKeyPath(path))
}
// WithJWTProfile creates a signer used for the JWT Profile Client Authentication on the token endpoint
// When creating the signer, be sure to include the KeyID in the SigningKey.
// See client.NewSignerFromPrivateKeyByte for an example.
func WithJWTProfile(signerFromKey SignerFromKey) Option {
return func(rp *relyingParty) error {
signer, err := signerFromKey()
if err != nil {
return err
}
rp.signer = signer
return nil
}
}
// 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)
func SignerFromKeyPath(path string) SignerFromKey {
return func() (jose.Signer, error) {
config, err := client.ConfigFromKeyFile(path)
if err != nil {
return nil, err
}
return client.NewSignerFromPrivateKeyByte([]byte(config.Key), config.KeyID)
}
}
func SignerFromKeyFile(fileData []byte) SignerFromKey {
return func() (jose.Signer, error) {
config, err := client.ConfigFromKeyFileData(fileData)
if err != nil {
return nil, err
}
return client.NewSignerFromPrivateKeyByte([]byte(config.Key), config.KeyID)
}
}
func SignerFromKeyAndKeyID(key []byte, keyID string) SignerFromKey {
return func() (jose.Signer, error) {
return client.NewSignerFromPrivateKeyByte(key, keyID)
}
}
// AuthURL returns the auth request url
// (wrapping the oauth2 `AuthCodeURL`)
func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string {
authOpts := make([]oauth2.AuthCodeOption, 0)
for _, opt := range opts {
authOpts = append(authOpts, opt()...)
}
return rp.OAuthConfig().AuthCodeURL(state, authOpts...)
}
// AuthURLHandler extends the `AuthURL` method with a http redirect handler
// 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, urlParam ...URLParamOpt) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
opts := make([]AuthURLOpt, len(urlParam))
for i, p := range urlParam {
opts[i] = AuthURLOpt(p)
}
state := stateFn()
if err := trySetStateCookie(w, state, rp); err != nil {
unauthorizedError(w, r, "failed to create state cookie: "+err.Error(), state, rp)
return
}
if rp.IsPKCE() {
codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp)
if err != nil {
unauthorizedError(w, r, "failed to create code challenge: "+err.Error(), state, rp)
return
}
opts = append(opts, WithCodeChallenge(codeChallenge))
}
http.Redirect(w, r, AuthURL(state, rp, opts...), http.StatusFound)
}
}
// GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie
func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (string, error) {
codeVerifier := base64.RawURLEncoding.EncodeToString([]byte(uuid.New().String()))
if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil {
return "", err
}
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
// 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) {
ctx, codeExchangeSpan := client.Tracer.Start(ctx, "CodeExchange")
defer codeExchangeSpan.End()
ctx = logCtxWithRPData(ctx, rp, "function", "CodeExchange")
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
codeOpts := make([]oauth2.AuthCodeOption, 0)
for _, opt := range opts {
codeOpts = append(codeOpts, opt()...)
}
ctx, oauthExchangeSpan := client.Tracer.Start(ctx, "OAuthExchange")
token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...)
if err != nil {
return nil, err
}
oauthExchangeSpan.End()
return verifyTokenResponse[C](ctx, token, rp)
}
// ClientCredentials requests an access token using the `client_credentials` grant,
// as defined in [RFC 6749, section 4.4].
//
// 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)
}
type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty)
// CodeExchangeHandler extends the `CodeExchange` method with a http handler
// including cookie handling for secure `state` transfer
// and optional PKCE code verifier checking.
// Custom parameters can optionally be set to the token URL.
func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
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)
if err != nil {
unauthorizedError(w, r, "failed to get state: "+err.Error(), state, rp)
return
}
if errValue := r.FormValue("error"); errValue != "" {
rp.ErrorHandler()(w, r, errValue, r.FormValue("error_description"), state)
return
}
codeOpts := make([]CodeExchangeOpt, len(urlParam))
for i, p := range urlParam {
codeOpts[i] = CodeExchangeOpt(p)
}
if rp.IsPKCE() {
codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode)
if err != nil {
unauthorizedError(w, r, "failed to get code verifier: "+err.Error(), state, rp)
return
}
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
rp.CookieHandler().DeleteCookie(w, pkceCode)
}
if rp.Signer() != nil {
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer())
if err != nil {
unauthorizedError(w, r, "failed to build assertion: "+err.Error(), state, rp)
return
}
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
}
tokens, err := CodeExchange[C](r.Context(), r.FormValue("code"), rp, codeOpts...)
if err != nil {
unauthorizedError(w, r, "failed to exchange token: "+err.Error(), state, rp)
return
}
callback(w, r, tokens, state, rp)
}
}
type SubjectGetter interface {
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
// and calls the userinfo endpoint with the access token
// 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] {
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) {
ctx, span := client.Tracer.Start(r.Context(), "UserinfoCallback")
r = r.WithContext(ctx)
defer span.End()
info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
if err != nil {
unauthorizedError(w, r, "userinfo failed: "+err.Error(), state, rp)
return
}
f(w, r, tokens, state, rp, info)
}
}
// Userinfo will call the OIDC [UserInfo] Endpoint with the provided token and returns
// the response in an instance of type U.
// [*oidc.UserInfo] can be used as a good example, or use a custom type if type-safe
// 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 {
return nilU, err
}
req.Header.Set("authorization", tokenType+" "+token)
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
return nilU, err
}
if userinfo.GetSubject() != subject {
return nilU, ErrUserInfoSubNotMatching
}
return userinfo, nil
}
func trySetStateCookie(w http.ResponseWriter, state string, rp RelyingParty) error {
if rp.CookieHandler() != nil {
if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil {
return err
}
}
return nil
}
func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelyingParty) (state string, err error) {
if rp.CookieHandler() == nil {
return r.FormValue(stateParam), nil
}
state, err = rp.CookieHandler().CheckQueryCookie(r, stateParam)
if err != nil {
return "", err
}
rp.CookieHandler().DeleteCookie(w, stateParam)
return state, nil
}
type OptionFunc func(RelyingParty)
type Endpoints struct {
oauth2.Endpoint
IntrospectURL string
UserinfoURL string
JKWsURL string
EndSessionURL string
RevokeURL string
DeviceAuthorizationURL string
}
func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
return Endpoints{
Endpoint: oauth2.Endpoint{
AuthURL: discoveryConfig.AuthorizationEndpoint,
TokenURL: discoveryConfig.TokenEndpoint,
},
IntrospectURL: discoveryConfig.IntrospectionEndpoint,
UserinfoURL: discoveryConfig.UserinfoEndpoint,
JKWsURL: discoveryConfig.JwksURI,
EndSessionURL: discoveryConfig.EndSessionEndpoint,
RevokeURL: discoveryConfig.RevocationEndpoint,
DeviceAuthorizationURL: discoveryConfig.DeviceAuthorizationEndpoint,
}
}
// 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
// WithCodeChallenge sets the `code_challenge` params in the auth request
func WithCodeChallenge(codeChallenge string) AuthURLOpt {
return func() []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
}
}
}
// WithPrompt sets the `prompt` params in the auth request
func WithPrompt(prompt ...string) AuthURLOpt {
return withPrompt(prompt...)
}
type CodeExchangeOpt func() []oauth2.AuthCodeOption
// WithCodeVerifier sets the `code_verifier` param in the token request
func WithCodeVerifier(codeVerifier string) CodeExchangeOpt {
return func() []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)}
}
}
// WithClientAssertionJWT sets the `client_assertion` param in the token request
func WithClientAssertionJWT(clientAssertion string) CodeExchangeOpt {
return func() []oauth2.AuthCodeOption {
return client.ClientAssertionCodeOptions(clientAssertion)
}
}
type tokenEndpointCaller struct {
RelyingParty
}
func (t tokenEndpointCaller) TokenEndpoint() string {
return t.OAuthConfig().Endpoint.TokenURL
}
type RefreshTokenRequest struct {
RefreshToken string `schema:"refresh_token"`
Scopes oidc.SpaceDelimitedArray `schema:"scope,omitempty"`
ClientID string `schema:"client_id,omitempty"`
ClientSecret string `schema:"client_secret,omitempty"`
ClientAssertion string `schema:"client_assertion,omitempty"`
ClientAssertionType string `schema:"client_assertion_type,omitempty"`
GrantType oidc.GrantType `schema:"grant_type"`
}
// RefreshTokens performs a token refresh. If it doesn't error, it will always
// 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{
RefreshToken: refreshToken,
Scopes: rp.OAuthConfig().Scopes,
ClientID: rp.OAuthConfig().ClientID,
ClientSecret: rp.OAuthConfig().ClientSecret,
ClientAssertion: clientAssertion,
ClientAssertionType: clientAssertionType,
GrantType: oidc.GrantTypeRefreshToken,
}
newToken, err := client.CallTokenEndpoint(ctx, 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) {
ctx = logCtxWithRPData(ctx, rp, "function", "EndSession")
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
defer span.End()
request := oidc.EndSessionRequest{
IdTokenHint: idToken,
ClientID: rp.OAuthConfig().ClientID,
PostLogoutRedirectURI: optionalRedirectURI,
State: optionalState,
}
return client.CallEndSessionEndpoint(ctx, request, nil, rp)
}
// RevokeToken requires a RelyingParty that is also a client.RevokeCaller. The RelyingParty
// returned by NewRelyingPartyOIDC() meets that criteria, but the one returned by
// NewRelyingPartyOAuth() does not.
//
// tokenTypeHint should be either "id_token" or "refresh_token".
func RevokeToken(ctx context.Context, 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{
Token: token,
TokenTypeHint: tokenTypeHint,
ClientID: rp.OAuthConfig().ClientID,
ClientSecret: rp.OAuthConfig().ClientSecret,
}
if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" {
return client.CallRevokeEndpoint(ctx, request, nil, rc)
}
return ErrRelyingPartyNotSupportRevokeCaller
}
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)
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/relying_party_test.go 0000664 0000000 0000000 00000005425 14656014552 0025654 0 ustar 00root root 0000000 0000000 package rp
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tu "github.com/zitadel/oidc/v3/internal/testutil"
"github.com/zitadel/oidc/v3/pkg/oidc"
"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)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/tockenexchange.go 0000664 0000000 0000000 00000001606 14656014552 0024710 0 ustar 00root root 0000000 0000000 package rp
import (
"context"
"golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/pkg/oidc/grants/tokenexchange"
)
// TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`
type TokenExchangeRP interface {
RelyingParty
// TokenExchange implement the `Token Exchange Grant` exchanging some token for an other
TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error)
}
// DelegationTokenExchangeRP extends the `TokenExchangeRP` interface
// for the specific `delegation token` request
type DelegationTokenExchangeRP interface {
TokenExchangeRP
// DelegationTokenExchange implement the `Token Exchange Grant`
// providing an access token in request for a `delegation` token for a given resource / audience
DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error)
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/userinfo_example_test.go 0000664 0000000 0000000 00000002122 14656014552 0026320 0 ustar 00root root 0000000 0000000 package rp_test
import (
"context"
"fmt"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/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)
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/verifier.go 0000664 0000000 0000000 00000011104 14656014552 0023527 0 ustar 00root root 0000000 0000000 package rp
import (
"context"
"time"
jose "github.com/go-jose/go-jose/v4"
"github.com/zitadel/oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
// VerifyTokens implement the Token Response Validation as defined in OIDC specification
// 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) {
ctx, span := client.Tracer.Start(ctx, "VerifyTokens")
defer span.End()
var nilClaims C
claims, err = VerifyIDToken[C](ctx, idToken, v)
if err != nil {
return nilClaims, err
}
if err := VerifyAccessToken(accessToken, claims.GetAccessTokenHash(), claims.GetSignatureAlgorithm()); err != nil {
return nilClaims, err
}
return claims, nil
}
// VerifyIDToken validates the id token according to
// 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) {
ctx, span := client.Tracer.Start(ctx, "VerifyIDToken")
defer span.End()
var nilClaims C
decrypted, err := oidc.DecryptToken(token)
if err != nil {
return nilClaims, err
}
payload, err := oidc.ParseToken(decrypted, &claims)
if err != nil {
return nilClaims, err
}
if err := oidc.CheckSubject(claims); err != nil {
return nilClaims, err
}
if err = oidc.CheckIssuer(claims, v.Issuer); err != nil {
return nilClaims, err
}
if err = oidc.CheckAudience(claims, v.ClientID); err != nil {
return nilClaims, err
}
if err = oidc.CheckAuthorizedParty(claims, v.ClientID); err != nil {
return nilClaims, err
}
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs, v.KeySet); err != nil {
return nilClaims, err
}
if err = oidc.CheckExpiration(claims, v.Offset); err != nil {
return nilClaims, err
}
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil {
return nilClaims, err
}
if v.Nonce != nil {
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
return nilClaims, err
}
}
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil {
return nilClaims, err
}
if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil {
return nilClaims, err
}
return claims, nil
}
type IDTokenVerifier oidc.Verifier
// VerifyAccessToken validates the access token according to
// https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
if atHash == "" {
return nil
}
actual, err := oidc.ClaimHash(accessToken, sigAlgorithm)
if err != nil {
return err
}
if actual != atHash {
return oidc.ErrAtHash
}
return nil
}
// NewIDTokenVerifier returns a oidc.Verifier suitable for ID token verification.
func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) *IDTokenVerifier {
v := &IDTokenVerifier{
Issuer: issuer,
ClientID: clientID,
KeySet: keySet,
Offset: time.Second,
Nonce: func(_ context.Context) string {
return ""
},
}
for _, opts := range options {
opts(v)
}
return v
}
// VerifierOption is the type for providing dynamic options to the IDTokenVerifier
type VerifierOption func(*IDTokenVerifier)
// 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
func WithIssuedAtOffset(offset time.Duration) VerifierOption {
return func(v *IDTokenVerifier) {
v.Offset = offset
}
}
// WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
func WithIssuedAtMaxAge(maxAge time.Duration) VerifierOption {
return func(v *IDTokenVerifier) {
v.MaxAgeIAT = maxAge
}
}
// WithNonce sets the function to check the nonce
func WithNonce(nonce func(context.Context) string) VerifierOption {
return func(v *IDTokenVerifier) {
v.Nonce = nonce
}
}
// WithACRVerifier sets the verifier for the acr claim
func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption {
return func(v *IDTokenVerifier) {
v.ACR = verifier
}
}
// WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now
func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption {
return func(v *IDTokenVerifier) {
v.MaxAge = maxAge
}
}
// WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm
func WithSupportedSigningAlgorithms(algs ...string) VerifierOption {
return func(v *IDTokenVerifier) {
v.SupportedSignAlgs = algs
}
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/verifier_test.go 0000664 0000000 0000000 00000022611 14656014552 0024573 0 ustar 00root root 0000000 0000000 package rp
import (
"context"
"testing"
"time"
jose "github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tu "github.com/zitadel/oidc/v3/internal/testutil"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
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)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rp/verifier_tokens_example_test.go 0000664 0000000 0000000 00000006671 14656014552 0027701 0 ustar 00root root 0000000 0000000 package rp_test
import (
"context"
"fmt"
tu "github.com/zitadel/oidc/v3/internal/testutil"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/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]
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rs/ 0000775 0000000 0000000 00000000000 14656014552 0021373 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/client/rs/introspect_example_test.go 0000664 0000000 0000000 00000003163 14656014552 0026671 0 ustar 00root root 0000000 0000000 package rs_test
import (
"context"
"fmt"
"github.com/zitadel/oidc/v3/pkg/client/rs"
"github.com/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)
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rs/resource_server.go 0000664 0000000 0000000 00000010143 14656014552 0025136 0 ustar 00root root 0000000 0000000 package rs
import (
"context"
"errors"
"net/http"
"time"
"github.com/zitadel/oidc/v3/pkg/client"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
type ResourceServer interface {
IntrospectionURL() string
TokenEndpoint() string
HttpClient() *http.Client
AuthFn() (any, error)
}
type resourceServer struct {
issuer string
tokenURL string
introspectURL string
httpClient *http.Client
authFn func() (any, error)
}
func (r *resourceServer) IntrospectionURL() string {
return r.introspectURL
}
func (r *resourceServer) TokenEndpoint() string {
return r.tokenURL
}
func (r *resourceServer) HttpClient() *http.Client {
return r.httpClient
}
func (r *resourceServer) AuthFn() (any, error) {
return r.authFn()
}
func NewResourceServerClientCredentials(ctx context.Context, issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) {
authorizer := func() (any, error) {
return httphelper.AuthorizeBasic(clientID, clientSecret), nil
}
return newResourceServer(ctx, issuer, authorizer, option...)
}
func NewResourceServerJWTProfile(ctx context.Context, issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) {
signer, err := client.NewSignerFromPrivateKeyByte(key, keyID)
if err != nil {
return nil, err
}
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 newResourceServer(ctx, issuer, authorizer, options...)
}
func newResourceServer(ctx context.Context, issuer string, authorizer func() (any, error), options ...Option) (*resourceServer, error) {
rs := &resourceServer{
issuer: issuer,
httpClient: httphelper.DefaultHTTPClient,
}
for _, optFunc := range options {
optFunc(rs)
}
if rs.introspectURL == "" || rs.tokenURL == "" {
config, err := client.Discover(ctx, rs.issuer, rs.httpClient)
if err != nil {
return nil, err
}
if rs.tokenURL == "" {
rs.tokenURL = config.TokenEndpoint
}
if rs.introspectURL == "" {
rs.introspectURL = config.IntrospectionEndpoint
}
}
if rs.tokenURL == "" {
return nil, errors.New("tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url")
}
rs.authFn = authorizer
return rs, nil
}
func NewResourceServerFromKeyFile(ctx context.Context, issuer, path string, options ...Option) (ResourceServer, error) {
c, err := client.ConfigFromKeyFile(path)
if err != nil {
return nil, err
}
return NewResourceServerJWTProfile(ctx, issuer, c.ClientID, c.KeyID, []byte(c.Key), options...)
}
type Option func(*resourceServer)
// WithClient provides the ability to set an http client to be used for the resource server
func WithClient(client *http.Client) Option {
return func(server *resourceServer) {
server.httpClient = client
}
}
// WithStaticEndpoints provides the ability to set static token and introspect URL
func WithStaticEndpoints(tokenURL, introspectURL string) Option {
return func(server *resourceServer) {
server.tokenURL = tokenURL
server.introspectURL = introspectURL
}
}
// Introspect calls the [RFC7662] Token Introspection
// 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()
if err != nil {
return resp, err
}
req, err := httphelper.FormRequest(ctx, rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn)
if err != nil {
return resp, err
}
if err := httphelper.HttpRequest(rp.HttpClient(), req, &resp); err != nil {
return resp, err
}
return resp, nil
}
golang-github-zitadel-oidc-3.27.0/pkg/client/rs/resource_server_test.go 0000664 0000000 0000000 00000011507 14656014552 0026202 0 ustar 00root root 0000000 0000000 package rs
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
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)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/client/tokenexchange/ 0000775 0000000 0000000 00000000000 14656014552 0023572 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/client/tokenexchange/tokenexchange.go 0000664 0000000 0000000 00000007602 14656014552 0026751 0 ustar 00root root 0000000 0000000 package tokenexchange
import (
"context"
"errors"
"net/http"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/oidc/v3/pkg/client"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
type TokenExchanger interface {
TokenEndpoint() string
HttpClient() *http.Client
AuthFn() (any, error)
}
type OAuthTokenExchange struct {
httpClient *http.Client
tokenEndpoint string
authFn func() (any, error)
}
func NewTokenExchanger(ctx context.Context, issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
return newOAuthTokenExchange(ctx, issuer, nil, options...)
}
func NewTokenExchangerClientCredentials(ctx context.Context, issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
authorizer := func() (any, error) {
return httphelper.AuthorizeBasic(clientID, clientSecret), nil
}
return newOAuthTokenExchange(ctx, issuer, authorizer, options...)
}
func NewTokenExchangerJWTProfile(ctx context.Context, issuer, clientID string, signer jose.Signer, options ...func(source *OAuthTokenExchange)) (TokenExchanger, 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{
httpClient: httphelper.DefaultHTTPClient,
}
for _, opt := range options {
opt(te)
}
if te.tokenEndpoint == "" {
config, err := client.Discover(ctx, issuer, te.httpClient)
if err != nil {
return nil, err
}
te.tokenEndpoint = config.TokenEndpoint
}
if te.tokenEndpoint == "" {
return nil, errors.New("tokenURL is empty: please provide with either `WithStaticTokenEndpoint` or a discovery url")
}
te.authFn = authorizer
return te, nil
}
func WithHTTPClient(client *http.Client) func(*OAuthTokenExchange) {
return func(source *OAuthTokenExchange) {
source.httpClient = client
}
}
func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*OAuthTokenExchange) {
return func(source *OAuthTokenExchange) {
source.tokenEndpoint = tokenEndpoint
}
}
func (te *OAuthTokenExchange) TokenEndpoint() string {
return te.tokenEndpoint
}
func (te *OAuthTokenExchange) HttpClient() *http.Client {
return te.httpClient
}
func (te *OAuthTokenExchange) AuthFn() (any, error) {
if te.authFn != nil {
return te.authFn()
}
return nil, nil
}
// ExchangeToken sends a token exchange request (rfc 8693) to te's token endpoint.
// SubjectToken and SubjectTokenType are required parameters.
func ExchangeToken(
ctx context.Context,
te TokenExchanger,
SubjectToken string,
SubjectTokenType oidc.TokenType,
ActorToken string,
ActorTokenType oidc.TokenType,
Resource []string,
Audience []string,
Scopes []string,
RequestedTokenType oidc.TokenType,
) (*oidc.TokenExchangeResponse, error) {
ctx, span := client.Tracer.Start(ctx, "ExchangeToken")
defer span.End()
if SubjectToken == "" {
return nil, errors.New("empty subject_token")
}
if SubjectTokenType == "" {
return nil, errors.New("empty subject_token_type")
}
authFn, err := te.AuthFn()
if err != nil {
return nil, err
}
request := oidc.TokenExchangeRequest{
GrantType: oidc.GrantTypeTokenExchange,
SubjectToken: SubjectToken,
SubjectTokenType: SubjectTokenType,
ActorToken: ActorToken,
ActorTokenType: ActorTokenType,
Resource: Resource,
Audience: Audience,
Scopes: Scopes,
RequestedTokenType: RequestedTokenType,
}
return client.CallTokenExchangeEndpoint(ctx, request, authFn, te)
}
golang-github-zitadel-oidc-3.27.0/pkg/crypto/ 0000775 0000000 0000000 00000000000 14656014552 0021011 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/crypto/crypto.go 0000664 0000000 0000000 00000003012 14656014552 0022654 0 ustar 00root root 0000000 0000000 package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
)
var ErrCipherTextBlockSize = errors.New("ciphertext block size is too short")
func EncryptAES(data string, key string) (string, error) {
encrypted, err := EncryptBytesAES([]byte(data), key)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(encrypted), nil
}
func EncryptBytesAES(plainText []byte, key string) ([]byte, error) {
block, err := aes.NewCipher([]byte(key))
if err != nil {
return nil, err
}
cipherText := make([]byte, aes.BlockSize+len(plainText))
iv := cipherText[:aes.BlockSize]
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(cipherText[aes.BlockSize:], plainText)
return cipherText, nil
}
func DecryptAES(data string, key string) (string, error) {
text, err := base64.RawURLEncoding.DecodeString(data)
if err != nil {
return "", err
}
decrypted, err := DecryptBytesAES(text, key)
if err != nil {
return "", err
}
return string(decrypted), nil
}
func DecryptBytesAES(cipherText []byte, key string) ([]byte, error) {
block, err := aes.NewCipher([]byte(key))
if err != nil {
return nil, err
}
if len(cipherText) < aes.BlockSize {
return nil, ErrCipherTextBlockSize
}
iv := cipherText[:aes.BlockSize]
cipherText = cipherText[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(cipherText, cipherText)
return cipherText, err
}
golang-github-zitadel-oidc-3.27.0/pkg/crypto/hash.go 0000664 0000000 0000000 00000001610 14656014552 0022261 0 ustar 00root root 0000000 0000000 package crypto
import (
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"errors"
"fmt"
"hash"
jose "github.com/go-jose/go-jose/v4"
)
var ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm")
func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) {
switch sigAlgorithm {
case jose.RS256, jose.ES256, jose.PS256:
return sha256.New(), nil
case jose.RS384, jose.ES384, jose.PS384:
return sha512.New384(), nil
case jose.RS512, jose.ES512, jose.PS512:
return sha512.New(), nil
default:
return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm)
}
}
func HashString(hash hash.Hash, s string, firstHalf bool) string {
if hash == nil {
return s
}
//nolint:errcheck
hash.Write([]byte(s))
size := hash.Size()
if firstHalf {
size = size / 2
}
sum := hash.Sum(nil)[:size]
return base64.RawURLEncoding.EncodeToString(sum)
}
golang-github-zitadel-oidc-3.27.0/pkg/crypto/key.go 0000664 0000000 0000000 00000002105 14656014552 0022126 0 ustar 00root root 0000000 0000000 package crypto
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"github.com/go-jose/go-jose/v4"
)
var (
ErrPEMDecode = errors.New("PEM decode failed")
ErrUnsupportedFormat = errors.New("key is neither in PKCS#1 nor PKCS#8 format")
ErrUnsupportedPrivateKey = errors.New("unsupported key type, must be RSA, ECDSA or ED25519 private key")
)
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 {
return nil, "", ErrUnsupportedFormat
}
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
}
}
golang-github-zitadel-oidc-3.27.0/pkg/crypto/key_test.go 0000664 0000000 0000000 00000010102 14656014552 0023161 0 ustar 00root root 0000000 0000000 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 "github.com/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)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/crypto/sign.go 0000664 0000000 0000000 00000001003 14656014552 0022272 0 ustar 00root root 0000000 0000000 package crypto
import (
"encoding/json"
"errors"
jose "github.com/go-jose/go-jose/v4"
)
func Sign(object any, signer jose.Signer) (string, error) {
payload, err := json.Marshal(object)
if err != nil {
return "", err
}
return SignPayload(payload, signer)
}
func SignPayload(payload []byte, signer jose.Signer) (string, error) {
if signer == nil {
return "", errors.New("missing signer")
}
result, err := signer.Sign(payload)
if err != nil {
return "", err
}
return result.CompactSerialize()
}
golang-github-zitadel-oidc-3.27.0/pkg/http/ 0000775 0000000 0000000 00000000000 14656014552 0020450 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/http/cookie.go 0000664 0000000 0000000 00000004560 14656014552 0022255 0 ustar 00root root 0000000 0000000 package http
import (
"errors"
"net/http"
"github.com/gorilla/securecookie"
)
type CookieHandler struct {
securecookie *securecookie.SecureCookie
secureOnly bool
sameSite http.SameSite
maxAge int
domain string
path string
}
func NewCookieHandler(hashKey, encryptKey []byte, opts ...CookieHandlerOpt) *CookieHandler {
c := &CookieHandler{
securecookie: securecookie.New(hashKey, encryptKey),
secureOnly: true,
sameSite: http.SameSiteLaxMode,
path: "/",
}
for _, opt := range opts {
opt(c)
}
return c
}
type CookieHandlerOpt func(*CookieHandler)
func WithUnsecure() CookieHandlerOpt {
return func(c *CookieHandler) {
c.secureOnly = false
}
}
func WithSameSite(sameSite http.SameSite) CookieHandlerOpt {
return func(c *CookieHandler) {
c.sameSite = sameSite
}
}
func WithMaxAge(maxAge int) CookieHandlerOpt {
return func(c *CookieHandler) {
c.maxAge = maxAge
c.securecookie.MaxAge(maxAge)
}
}
func WithDomain(domain string) CookieHandlerOpt {
return func(c *CookieHandler) {
c.domain = domain
}
}
func WithPath(path string) CookieHandlerOpt {
return func(c *CookieHandler) {
c.path = path
}
}
func (c *CookieHandler) CheckCookie(r *http.Request, name string) (string, error) {
cookie, err := r.Cookie(name)
if err != nil {
return "", err
}
var value string
if err := c.securecookie.Decode(name, cookie.Value, &value); err != nil {
return "", err
}
return value, nil
}
func (c *CookieHandler) CheckQueryCookie(r *http.Request, name string) (string, error) {
value, err := c.CheckCookie(r, name)
if err != nil {
return "", err
}
if value != r.FormValue(name) {
return "", errors.New(name + " does not compare")
}
return value, nil
}
func (c *CookieHandler) SetCookie(w http.ResponseWriter, name, value string) error {
encoded, err := c.securecookie.Encode(name, value)
if err != nil {
return err
}
http.SetCookie(w, &http.Cookie{
Name: name,
Value: encoded,
Domain: c.domain,
Path: c.path,
MaxAge: c.maxAge,
HttpOnly: true,
Secure: c.secureOnly,
SameSite: c.sameSite,
})
return nil
}
func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, name string) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: "",
Domain: c.domain,
Path: c.path,
MaxAge: -1,
HttpOnly: true,
Secure: c.secureOnly,
SameSite: c.sameSite,
})
}
golang-github-zitadel-oidc-3.27.0/pkg/http/http.go 0000664 0000000 0000000 00000005017 14656014552 0021761 0 ustar 00root root 0000000 0000000 package http
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
var DefaultHTTPClient = &http.Client{
Timeout: 30 * time.Second,
}
type Decoder interface {
Decode(dst any, src map[string][]string) error
}
type Encoder interface {
Encode(src any, dst map[string][]string) error
}
type FormAuthorization func(url.Values)
type RequestAuthorization func(*http.Request)
func AuthorizeBasic(user, password string) RequestAuthorization {
return func(req *http.Request) {
req.SetBasicAuth(url.QueryEscape(user), url.QueryEscape(password))
}
}
func FormRequest(ctx context.Context, endpoint string, request any, encoder Encoder, authFn any) (*http.Request, error) {
form := url.Values{}
if err := encoder.Encode(request, form); err != nil {
return nil, err
}
if fn, ok := authFn.(FormAuthorization); ok {
fn(form)
}
body := strings.NewReader(form.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
if err != nil {
return nil, err
}
if fn, ok := authFn.(RequestAuthorization); ok {
fn(req)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req, nil
}
func HttpRequest(client *http.Client, req *http.Request, response any) error {
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("unable to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
var oidcErr oidc.Error
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)
if err != nil {
return fmt.Errorf("failed to unmarshal response: %v %s", err, body)
}
return nil
}
func URLEncodeParams(resp any, encoder Encoder) (url.Values, error) {
values := make(map[string][]string)
err := encoder.Encode(resp, values)
if err != nil {
return nil, err
}
return values, nil
}
func StartServer(ctx context.Context, port string) {
server := &http.Server{Addr: port}
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("ListenAndServe(): %v", err)
}
}()
go func() {
<-ctx.Done()
ctxShutdown, cancelShutdown := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelShutdown()
err := server.Shutdown(ctxShutdown)
if err != nil {
log.Fatalf("Shutdown(): %v", err)
}
}()
}
golang-github-zitadel-oidc-3.27.0/pkg/http/marshal.go 0000664 0000000 0000000 00000001762 14656014552 0022434 0 ustar 00root root 0000000 0000000 package http
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"reflect"
)
func MarshalJSON(w http.ResponseWriter, i any) {
MarshalJSONWithStatus(w, i, http.StatusOK)
}
func MarshalJSONWithStatus(w http.ResponseWriter, i any, status int) {
w.Header().Set("content-type", "application/json")
w.WriteHeader(status)
if i == nil || (reflect.ValueOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) {
return
}
err := json.NewEncoder(w).Encode(i)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func ConcatenateJSON(first, second []byte) ([]byte, error) {
if !bytes.HasSuffix(first, []byte{'}'}) {
return nil, fmt.Errorf("jws: invalid JSON %s", first)
}
if !bytes.HasPrefix(second, []byte{'{'}) {
return nil, fmt.Errorf("jws: invalid JSON %s", second)
}
// check empty
if len(first) == 2 {
return second, nil
}
if len(second) == 2 {
return first, nil
}
first[len(first)-1] = ','
first = append(first, second[1:]...)
return first, nil
}
golang-github-zitadel-oidc-3.27.0/pkg/http/marshal_test.go 0000664 0000000 0000000 00000004522 14656014552 0023470 0 ustar 00root root 0000000 0000000 package http
import (
"bytes"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConcatenateJSON(t *testing.T) {
type args struct {
first []byte
second []byte
}
tests := []struct {
name string
args args
want []byte
wantErr bool
}{
{
"invalid first part, error",
args{
[]byte(`invalid`),
[]byte(`{"some": "thing"}`),
},
nil,
true,
},
{
"invalid second part, error",
args{
[]byte(`{"some": "thing"}`),
[]byte(`invalid`),
},
nil,
true,
},
{
"both valid, merged",
args{
[]byte(`{"some": "thing"}`),
[]byte(`{"another": "thing"}`),
},
[]byte(`{"some": "thing","another": "thing"}`),
false,
},
{
"first empty",
args{
[]byte(`{}`),
[]byte(`{"some": "thing"}`),
},
[]byte(`{"some": "thing"}`),
false,
},
{
"second empty",
args{
[]byte(`{"some": "thing"}`),
[]byte(`{}`),
},
[]byte(`{"some": "thing"}`),
false,
},
{
"both empty",
args{
[]byte(`{}`),
[]byte(`{}`),
},
[]byte(`{}`),
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ConcatenateJSON(tt.args.first, tt.args.second)
if (err != nil) != tt.wantErr {
t.Errorf("ConcatenateJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !bytes.Equal(got, tt.want) {
t.Errorf("ConcatenateJSON() got = %v, want %v", string(got), tt.want)
}
})
}
}
func TestMarshalJSONWithStatus(t *testing.T) {
type args struct {
i any
status int
}
type res struct {
statusCode int
body string
}
tests := []struct {
name string
args args
res res
}{
{
"empty ok",
args{
nil,
200,
},
res{
200,
"",
},
},
{
"string ok",
args{
"ok",
200,
},
res{
200,
`"ok"
`,
},
},
{
"struct ok",
args{
struct {
Test string `json:"test"`
}{"ok"},
200,
},
res{
200,
`{"test":"ok"}
`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
MarshalJSONWithStatus(w, tt.args.i, tt.args.status)
assert.Equal(t, tt.res.statusCode, w.Result().StatusCode)
assert.Equal(t, "application/json", w.Header().Get("content-type"))
assert.Equal(t, tt.res.body, w.Body.String())
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/ 0000775 0000000 0000000 00000000000 14656014552 0020407 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/oidc/authorization.go 0000664 0000000 0000000 00000012003 14656014552 0023632 0 ustar 00root root 0000000 0000000 package oidc
import (
"log/slog"
)
const (
// ScopeOpenID defines the scope `openid`
// OpenID Connect requests MUST contain the `openid` scope value
ScopeOpenID = "openid"
// ScopeProfile defines the scope `profile`
// This (optional) scope value requests access to the End-User's default profile Claims,
// which are: name, family_name, given_name, middle_name, nickname, preferred_username,
// profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at.
ScopeProfile = "profile"
// ScopeEmail defines the scope `email`
// This (optional) scope value requests access to the email and email_verified Claims.
ScopeEmail = "email"
// ScopeAddress defines the scope `address`
// This (optional) scope value requests access to the address Claim.
ScopeAddress = "address"
// ScopePhone defines the scope `phone`
// This (optional) scope value requests access to the phone_number and phone_number_verified Claims.
ScopePhone = "phone"
// ScopeOfflineAccess defines the scope `offline_access`
// This (optional) scope value requests that an OAuth 2.0 Refresh Token be issued that can be used to obtain an Access Token
// that grants access to the End-User's UserInfo Endpoint even when the End-User is not present (not logged in).
ScopeOfflineAccess = "offline_access"
// ResponseTypeCode for the Authorization Code Flow returning a code from the Authorization Server
ResponseTypeCode ResponseType = "code"
// ResponseTypeIDToken for the Implicit Flow returning id and access tokens directly from the Authorization Server
ResponseTypeIDToken ResponseType = "id_token token"
// ResponseTypeIDTokenOnly for the Implicit Flow returning only id token directly from the Authorization Server
ResponseTypeIDTokenOnly ResponseType = "id_token"
DisplayPage Display = "page"
DisplayPopup Display = "popup"
DisplayTouch Display = "touch"
DisplayWAP Display = "wap"
ResponseModeQuery ResponseMode = "query"
ResponseModeFragment ResponseMode = "fragment"
ResponseModeFormPost ResponseMode = "form_post"
// 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
PromptNone = "none"
// PromptLogin (`login`) directs the Authorization Server to prompt the End-User for reauthentication.
PromptLogin = "login"
// PromptConsent (`consent`) directs the Authorization Server to prompt the End-User for consent (of sharing information).
PromptConsent = "consent"
// PromptSelectAccount (`select_account `) directs the Authorization Server to prompt the End-User to select a user account (to enable multi user / session switching)
PromptSelectAccount = "select_account"
)
// AuthRequest according to:
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
type AuthRequest struct {
Scopes SpaceDelimitedArray `json:"scope" schema:"scope"`
ResponseType ResponseType `json:"response_type" schema:"response_type"`
ClientID string `json:"client_id" schema:"client_id"`
RedirectURI string `json:"redirect_uri" schema:"redirect_uri"`
State string `json:"state" schema:"state"`
Nonce string `json:"nonce" schema:"nonce"`
ResponseMode ResponseMode `json:"response_mode" schema:"response_mode"`
Display Display `json:"display" schema:"display"`
Prompt SpaceDelimitedArray `json:"prompt" schema:"prompt"`
MaxAge *uint `json:"max_age" schema:"max_age"`
UILocales Locales `json:"ui_locales" schema:"ui_locales"`
IDTokenHint string `json:"id_token_hint" schema:"id_token_hint"`
LoginHint string `json:"login_hint" schema:"login_hint"`
ACRValues SpaceDelimitedArray `json:"acr_values" schema:"acr_values"`
CodeChallenge string `json:"code_challenge" schema:"code_challenge"`
CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method" schema:"code_challenge_method"`
// RequestParam enables OIDC requests to be passed in a single, self-contained parameter (as JWT, called Request Object)
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
func (a *AuthRequest) GetRedirectURI() string {
return a.RedirectURI
}
// GetResponseType returns the response_type value for the ErrAuthRequest interface
func (a *AuthRequest) GetResponseType() ResponseType {
return a.ResponseType
}
// GetState returns the optional state value for the ErrAuthRequest interface
func (a *AuthRequest) GetState() string {
return a.State
}
// GetResponseMode returns the optional ResponseMode
func (a *AuthRequest) GetResponseMode() ResponseMode {
return a.ResponseMode
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/authorization_test.go 0000664 0000000 0000000 00000001123 14656014552 0024672 0 ustar 00root root 0000000 0000000 //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)
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/code_challenge.go 0000664 0000000 0000000 00000001225 14656014552 0023652 0 ustar 00root root 0000000 0000000 package oidc
import (
"crypto/sha256"
"github.com/zitadel/oidc/v3/pkg/crypto"
)
const (
CodeChallengeMethodPlain CodeChallengeMethod = "plain"
CodeChallengeMethodS256 CodeChallengeMethod = "S256"
)
type CodeChallengeMethod string
type CodeChallenge struct {
Challenge string
Method CodeChallengeMethod
}
func NewSHACodeChallenge(code string) string {
return crypto.HashString(sha256.New(), code, false)
}
func VerifyCodeChallenge(c *CodeChallenge, codeVerifier string) bool {
if c == nil {
return false
}
if c.Method == CodeChallengeMethodS256 {
codeVerifier = NewSHACodeChallenge(codeVerifier)
}
return codeVerifier == c.Challenge
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/device_authorization.go 0000664 0000000 0000000 00000003303 14656014552 0025154 0 ustar 00root root 0000000 0000000 package oidc
import "encoding/json"
// DeviceAuthorizationRequest implements
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1,
// 3.1 Device Authorization Request.
type DeviceAuthorizationRequest struct {
Scopes SpaceDelimitedArray `schema:"scope"`
ClientID string `schema:"client_id"`
}
// DeviceAuthorizationResponse implements
// https://www.rfc-editor.org/rfc/rfc8628#section-3.2
// 3.2. Device Authorization Response.
type DeviceAuthorizationResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
ExpiresIn int `json:"expires_in"`
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
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4,
// Device Access Token Request.
type DeviceAccessTokenRequest struct {
GrantType GrantType `json:"grant_type" schema:"grant_type"`
DeviceCode string `json:"device_code" schema:"device_code"`
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/device_authorization_test.go 0000664 0000000 0000000 00000001222 14656014552 0026211 0 ustar 00root root 0000000 0000000 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)
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/discovery.go 0000664 0000000 0000000 00000025262 14656014552 0022754 0 ustar 00root root 0000000 0000000 package oidc
const (
DiscoveryEndpoint = "/.well-known/openid-configuration"
)
type DiscoveryConfiguration struct {
// Issuer is the identifier of the OP and is used in the tokens as `iss` claim.
Issuer string `json:"issuer,omitempty"`
// AuthorizationEndpoint is the URL of the OAuth 2.0 Authorization Endpoint where all user interactive login start
AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"`
// TokenEndpoint is the URL of the OAuth 2.0 Token Endpoint where all tokens are issued, except when using Implicit Flow
TokenEndpoint string `json:"token_endpoint,omitempty"`
// IntrospectionEndpoint is the URL of the OAuth 2.0 Introspection Endpoint.
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
// UserinfoEndpoint is the URL where an access_token can be used to retrieve the Userinfo.
UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"`
// RevocationEndpoint is the URL of the OAuth 2.0 Revocation Endpoint.
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
// EndSessionEndpoint is a URL where the RP can perform a redirect to request that the End-User be logged out at the OP.
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint,omitempty"`
// CheckSessionIframe is a URL where the OP provides an iframe that support cross-origin communications for session state information with the RP Client.
CheckSessionIframe string `json:"check_session_iframe,omitempty"`
// JwksURI is the URL of the JSON Web Key Set. This site contains the signing keys that RPs can use to validate the signature.
// It may also contain the OP's encryption keys that RPs can use to encrypt request to the OP.
JwksURI string `json:"jwks_uri,omitempty"`
// RegistrationEndpoint is the URL for the Dynamic Client Registration.
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
// ScopesSupported lists an array of supported scopes. This list must not include every supported scope by the OP.
ScopesSupported []string `json:"scopes_supported,omitempty"`
// ResponseTypesSupported contains a list of the OAuth 2.0 response_type values that the OP supports (code, id_token, token id_token, ...).
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
// ResponseModesSupported contains a list of the OAuth 2.0 response_mode values that the OP supports. If omitted, the default value is ["query", "fragment"].
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
// GrantTypesSupported contains a list of the OAuth 2.0 grant_type values that the OP supports. If omitted, the default value is ["authorization_code", "implicit"].
GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"`
// ACRValuesSupported contains a list of Authentication Context Class References that the OP supports.
ACRValuesSupported []string `json:"acr_values_supported,omitempty"`
// SubjectTypesSupported contains a list of Subject Identifier types that the OP supports (pairwise, public).
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
// IDTokenSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for the ID Token.
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
// IDTokenEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for the ID Token.
IDTokenEncryptionAlgValuesSupported []string `json:"id_token_encryption_alg_values_supported,omitempty"`
// IDTokenEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for the ID Token.
IDTokenEncryptionEncValuesSupported []string `json:"id_token_encryption_enc_values_supported,omitempty"`
// UserinfoSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for UserInfo Endpoint.
UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported,omitempty"`
// UserinfoEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for the UserInfo Endpoint.
UserinfoEncryptionAlgValuesSupported []string `json:"userinfo_encryption_alg_values_supported,omitempty"`
// UserinfoEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for the UserInfo Endpoint.
UserinfoEncryptionEncValuesSupported []string `json:"userinfo_encryption_enc_values_supported,omitempty"`
// RequestObjectSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the OP for Request Objects.
// These algorithms are used both then the Request Object is passed by value (using the request parameter) and when it is passed by reference (using the request_uri parameter).
RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported,omitempty"`
// RequestObjectEncryptionAlgValuesSupported contains a list of JWE encryption algorithms (alg values) supported by the OP for Request Objects.
// These algorithms are used both when the Request Object is passed by value and by reference.
RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported,omitempty"`
// RequestObjectEncryptionEncValuesSupported contains a list of JWE encryption algorithms (enc values) supported by the OP for Request Objects.
// These algorithms are used both when the Request Object is passed by value and by reference.
RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported,omitempty"`
// TokenEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Token Endpoint. If omitted, the default is client_secret_basic.
TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"`
// TokenEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Token Endpoint
// for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt.
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
// RevocationEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Revocation Endpoint. If omitted, the default is client_secret_basic.
RevocationEndpointAuthMethodsSupported []AuthMethod `json:"revocation_endpoint_auth_methods_supported,omitempty"`
// RevocationEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Revocation Endpoint
// for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt.
RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"`
// IntrospectionEndpointAuthMethodsSupported contains a list of Client Authentication methods supported by the Introspection Endpoint.
IntrospectionEndpointAuthMethodsSupported []AuthMethod `json:"introspection_endpoint_auth_methods_supported,omitempty"`
// IntrospectionEndpointAuthSigningAlgValuesSupported contains a list of JWS signing algorithms (alg values) supported by the Revocation Endpoint
// for the signature of the JWT used to authenticate the Client by private_key_jwt and client_secret_jwt.
IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"`
// DisplayValuesSupported contains a list of display parameter values that the OP supports (page, popup, touch, wap).
DisplayValuesSupported []Display `json:"display_values_supported,omitempty"`
// ClaimTypesSupported contains a list of Claim Types that the OP supports (normal, aggregated, distributed). If omitted, the default is normal Claims.
ClaimTypesSupported []string `json:"claim_types_supported,omitempty"`
// ClaimsSupported contains a list of Claim Names the OP may be able to supply values for. This list might not be exhaustive.
ClaimsSupported []string `json:"claims_supported,omitempty"`
// ClaimsParameterSupported specifies whether the OP supports use of the `claims` parameter. If omitted, the default is false.
ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"`
// CodeChallengeMethodsSupported contains a list of Proof Key for Code Exchange (PKCE) code challenge methods supported by the OP.
CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"`
// ServiceDocumentation is a URL where developers can get information about the OP and its usage.
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 Locales `json:"claims_locales_supported,omitempty"`
// UILocalesSupported contains a list of BCP47 language tag values that the OP supports for the user interface.
UILocalesSupported Locales `json:"ui_locales_supported,omitempty"`
// 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"`
// RequestURIParameterSupported specifies whether the OP supports use of the `request_uri` parameter. If omitted, the default value is true. (therefore no omitempty)
RequestURIParameterSupported bool `json:"request_uri_parameter_supported"`
// RequireRequestURIRegistration specifies whether the OP requires any `request_uri` to be pre-registered using the request_uris registration parameter. If omitted, the default value is false.
RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"`
// OPPolicyURI is a URL the OP provides to the person registering the Client to read about the OP's requirements on how the RP can use the data provided by the OP.
OPPolicyURI string `json:"op_policy_uri,omitempty"`
// 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"`
}
type AuthMethod string
const (
AuthMethodBasic AuthMethod = "client_secret_basic"
AuthMethodPost AuthMethod = "client_secret_post"
AuthMethodNone AuthMethod = "none"
AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt"
)
var AllAuthMethods = []AuthMethod{
AuthMethodBasic, AuthMethodPost, AuthMethodNone, AuthMethodPrivateKeyJWT,
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/error.go 0000664 0000000 0000000 00000014773 14656014552 0022103 0 ustar 00root root 0000000 0000000 package oidc
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
)
type errorType string
const (
InvalidRequest errorType = "invalid_request"
InvalidScope errorType = "invalid_scope"
InvalidClient errorType = "invalid_client"
InvalidGrant errorType = "invalid_grant"
UnauthorizedClient errorType = "unauthorized_client"
UnsupportedGrantType errorType = "unsupported_grant_type"
ServerError errorType = "server_error"
InteractionRequired errorType = "interaction_required"
LoginRequired errorType = "login_required"
RequestNotSupported errorType = "request_not_supported"
// Additional error codes as defined in
// https://www.rfc-editor.org/rfc/rfc8628#section-3.5
// Device Access Token Response
AuthorizationPending errorType = "authorization_pending"
SlowDown errorType = "slow_down"
AccessDenied errorType = "access_denied"
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 (
ErrInvalidRequest = func() *Error {
return &Error{
ErrorType: InvalidRequest,
}
}
ErrInvalidRequestRedirectURI = func() *Error {
return &Error{
ErrorType: InvalidRequest,
redirectDisabled: true,
}
}
ErrInvalidScope = func() *Error {
return &Error{
ErrorType: InvalidScope,
}
}
ErrInvalidClient = func() *Error {
return &Error{
ErrorType: InvalidClient,
}
}
ErrInvalidGrant = func() *Error {
return &Error{
ErrorType: InvalidGrant,
}
}
ErrUnauthorizedClient = func() *Error {
return &Error{
ErrorType: UnauthorizedClient,
}
}
ErrUnsupportedGrantType = func() *Error {
return &Error{
ErrorType: UnsupportedGrantType,
}
}
ErrServerError = func() *Error {
return &Error{
ErrorType: ServerError,
}
}
ErrInteractionRequired = func() *Error {
return &Error{
ErrorType: InteractionRequired,
}
}
ErrLoginRequired = func() *Error {
return &Error{
ErrorType: LoginRequired,
}
}
ErrRequestNotSupported = func() *Error {
return &Error{
ErrorType: RequestNotSupported,
}
}
// Device Access Token errors:
ErrAuthorizationPending = func() *Error {
return &Error{
ErrorType: AuthorizationPending,
Description: "The client SHOULD repeat the access token request to the token endpoint, after interval from device authorization response.",
}
}
ErrSlowDown = func() *Error {
return &Error{
ErrorType: SlowDown,
Description: "Polling should continue, but the interval MUST be increased by 5 seconds for this and all subsequent requests.",
}
}
ErrAccessDenied = func() *Error {
return &Error{
ErrorType: AccessDenied,
Description: "The authorization request was denied.",
}
}
ErrExpiredDeviceCode = func() *Error {
return &Error{
ErrorType: ExpiredToken,
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 {
Parent error `json:"-" schema:"-"`
ErrorType errorType `json:"error" schema:"error"`
Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
State string `json:"state,omitempty" schema:"state,omitempty"`
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"`
Parent string `json:"parent,omitempty"`
}{
Error: e.ErrorType,
ErrorDescription: e.Description,
State: e.State,
}
if e.returnParent {
m.Parent = e.Parent.Error()
}
return json.Marshal(m)
}
func (e *Error) Error() string {
message := "ErrorType=" + string(e.ErrorType)
if e.Description != "" {
message += " Description=" + e.Description
}
if e.Parent != nil {
message += " Parent=" + e.Parent.Error()
}
return message
}
func (e *Error) Unwrap() error {
return e.Parent
}
func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
if !ok {
return false
}
return e.ErrorType == t.ErrorType &&
(e.Description == t.Description || t.Description == "") &&
(e.State == t.State || t.State == "")
}
func (e *Error) WithParent(err error) *Error {
e.Parent = err
return e
}
// WithReturnParentToClient allows returning the set parent error to the HTTP client.
// 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...)
return e
}
func (e *Error) IsRedirectDisabled() bool {
return e.redirectDisabled
}
// DefaultToServerError checks if the error is an Error
// if not the provided error will be wrapped into a ServerError
func DefaultToServerError(err error, description string) *Error {
oauth := new(Error)
if ok := errors.As(err, &oauth); !ok {
oauth.ErrorType = ServerError
oauth.Description = description
oauth.Parent = err
}
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.redirectDisabled {
attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled))
}
return slog.GroupValue(attrs...)
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/error_test.go 0000664 0000000 0000000 00000007565 14656014552 0023143 0 ustar 00root root 0000000 0000000 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))
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/grants/ 0000775 0000000 0000000 00000000000 14656014552 0021705 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/oidc/grants/client_credentials.go 0000664 0000000 0000000 00000002074 14656014552 0026072 0 ustar 00root root 0000000 0000000 package grants
import "strings"
type clientCredentialsGrantBasic struct {
grantType string `schema:"grant_type"`
scope string `schema:"scope"`
}
type clientCredentialsGrant struct {
*clientCredentialsGrantBasic
clientID string `schema:"client_id"`
clientSecret string `schema:"client_secret"`
}
// ClientCredentialsGrantBasic creates an oauth2 `Client Credentials` Grant
// sending client_id and client_secret as basic auth header
func ClientCredentialsGrantBasic(scopes ...string) *clientCredentialsGrantBasic {
return &clientCredentialsGrantBasic{
grantType: "client_credentials",
scope: strings.Join(scopes, " "),
}
}
// ClientCredentialsGrantValues creates an oauth2 `Client Credentials` Grant
// sending client_id and client_secret as form values
func ClientCredentialsGrantValues(clientID, clientSecret string, scopes ...string) *clientCredentialsGrant {
return &clientCredentialsGrant{
clientCredentialsGrantBasic: ClientCredentialsGrantBasic(scopes...),
clientID: clientID,
clientSecret: clientSecret,
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/grants/tokenexchange/ 0000775 0000000 0000000 00000000000 14656014552 0024530 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/oidc/grants/tokenexchange/tokenexchange.go 0000664 0000000 0000000 00000004306 14656014552 0027705 0 ustar 00root root 0000000 0000000 package tokenexchange
const (
AccessTokenType = "urn:ietf:params:oauth:token-type:access_token"
RefreshTokenType = "urn:ietf:params:oauth:token-type:refresh_token"
IDTokenType = "urn:ietf:params:oauth:token-type:id_token"
JWTTokenType = "urn:ietf:params:oauth:token-type:jwt"
DelegationTokenType = AccessTokenType
TokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
)
type TokenExchangeRequest struct {
grantType string `schema:"grant_type"`
subjectToken string `schema:"subject_token"`
subjectTokenType string `schema:"subject_token_type"`
actorToken string `schema:"actor_token"`
actorTokenType string `schema:"actor_token_type"`
resource []string `schema:"resource"`
audience []string `schema:"audience"`
scope []string `schema:"scope"`
requestedTokenType string `schema:"requested_token_type"`
}
func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest {
t := &TokenExchangeRequest{
grantType: TokenExchangeGrantType,
subjectToken: subjectToken,
subjectTokenType: subjectTokenType,
requestedTokenType: AccessTokenType,
}
for _, opt := range opts {
opt(t)
}
return t
}
type TokenExchangeOption func(*TokenExchangeRequest)
func WithActorToken(token, tokenType string) func(*TokenExchangeRequest) {
return func(req *TokenExchangeRequest) {
req.actorToken = token
req.actorTokenType = tokenType
}
}
func WithAudience(audience []string) func(*TokenExchangeRequest) {
return func(req *TokenExchangeRequest) {
req.audience = audience
}
}
func WithGrantType(grantType string) TokenExchangeOption {
return func(req *TokenExchangeRequest) {
req.grantType = grantType
}
}
func WithRequestedTokenType(tokenType string) func(*TokenExchangeRequest) {
return func(req *TokenExchangeRequest) {
req.requestedTokenType = tokenType
}
}
func WithResource(resource []string) func(*TokenExchangeRequest) {
return func(req *TokenExchangeRequest) {
req.resource = resource
}
}
func WithScope(scope []string) func(*TokenExchangeRequest) {
return func(req *TokenExchangeRequest) {
req.scope = scope
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/introspection.go 0000664 0000000 0000000 00000005666 14656014552 0023653 0 ustar 00root root 0000000 0000000 package oidc
import "github.com/muhlemmer/gu"
type IntrospectionRequest struct {
Token string `schema:"token"`
}
type ClientAssertionParams struct {
ClientAssertion string `schema:"client_assertion"`
ClientAssertionType string `schema:"client_assertion_type"`
}
// IntrospectionResponse implements RFC 7662, section 2.2 and
// OpenID Connect Core 1.0, section 5.1 (UserInfo).
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2.
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims.
type IntrospectionResponse struct {
Active bool `json:"active"`
Scope SpaceDelimitedArray `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
TokenType string `json:"token_type,omitempty"`
Expiration Time `json:"exp,omitempty"`
IssuedAt Time `json:"iat,omitempty"`
AuthTime Time `json:"auth_time,omitempty"`
NotBefore Time `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
Audience Audience `json:"aud,omitempty"`
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
Issuer string `json:"iss,omitempty"`
JWTID string `json:"jti,omitempty"`
Username string `json:"username,omitempty"`
Actor *ActorClaims `json:"act,omitempty"`
UserInfoProfile
UserInfoEmail
UserInfoPhone
Address *UserInfoAddress `json:"address,omitempty"`
Claims map[string]any `json:"-"`
}
// SetUserInfo copies all relevant fields from UserInfo
// into the 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
// care of a possible nil value.
func (i *IntrospectionResponse) GetAddress() *UserInfoAddress {
if i.Address == nil {
return new(UserInfoAddress)
}
return i.Address
}
// introspectionResponseAlias prevents loops on the JSON methods
type introspectionResponseAlias IntrospectionResponse
func (i *IntrospectionResponse) MarshalJSON() ([]byte, error) {
if i.Username == "" {
i.Username = i.PreferredUsername
}
return mergeAndMarshalClaims((*introspectionResponseAlias)(i), i.Claims)
}
func (i *IntrospectionResponse) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*introspectionResponseAlias)(i), &i.Claims)
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/introspection_test.go 0000664 0000000 0000000 00000003776 14656014552 0024712 0 ustar 00root root 0000000 0000000 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"}`)
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/jwt_profile.go 0000664 0000000 0000000 00000001113 14656014552 0023256 0 ustar 00root root 0000000 0000000 package oidc
type JWTProfileGrantRequest struct {
Assertion string `schema:"assertion"`
Scope SpaceDelimitedArray `schema:"scope"`
GrantType GrantType `schema:"grant_type"`
}
// NewJWTProfileGrantRequest creates an oauth2 `JSON Web Token (JWT) Profile` Grant
//`urn:ietf:params:oauth:grant-type:jwt-bearer`
// sending a self-signed jwt as assertion
func NewJWTProfileGrantRequest(assertion string, scopes ...string) *JWTProfileGrantRequest {
return &JWTProfileGrantRequest{
GrantType: GrantTypeBearer,
Assertion: assertion,
Scope: scopes,
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/keyset.go 0000664 0000000 0000000 00000006304 14656014552 0022245 0 ustar 00root root 0000000 0000000 package oidc
import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"errors"
jose "github.com/go-jose/go-jose/v4"
)
const (
KeyUseSignature = "sig"
)
var (
ErrKeyMultiple = errors.New("multiple possible keys match")
ErrKeyNone = errors.New("no possible keys matches")
)
// KeySet represents a set of JSON Web Keys
// - remotely fetch via discovery and jwks_uri -> `remoteKeySet`
// - held by the OP itself in storage -> `openIDKeySet`
// - dynamically aggregated by request for OAuth JWT Profile Assertion -> `jwtProfileKeySet`
type KeySet interface {
// VerifySignature verifies the signature with the given keyset and returns the raw payload
VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error)
}
// GetKeyIDAndAlg returns the `kid` and `alg` claim from the JWS header
func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) {
keyID := ""
alg := ""
for _, sig := range jws.Signatures {
keyID = sig.Header.KeyID
alg = sig.Header.Algorithm
break
}
return keyID, alg
}
// FindKey searches the given JSON Web Keys for the requested key ID, usage and key type
//
// will return the key immediately if matches exact (id, usage, type)
//
// will return false none or multiple match
//
// deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool
// moved implementation already to FindMatchingKey
func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) {
key, err := FindMatchingKey(keyID, use, expectedAlg, keys...)
return key, err == nil
}
// FindMatchingKey searches the given JSON Web Keys for the requested key ID, usage and alg type
//
// will return the key immediately if matches exact (id, usage, type)
//
// will return a specific error if none (ErrKeyNone) or multiple (ErrKeyMultiple) match
func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (key jose.JSONWebKey, err error) {
var validKeys []jose.JSONWebKey
for _, k := range keys {
// ignore all keys with wrong use (let empty use of published key pass)
if k.Use != use && k.Use != "" {
continue
}
// ignore all keys with wrong algorithm type
if !algToKeyType(k.Key, expectedAlg) {
continue
}
// if we get here, use and alg match, so an equal (not empty) keyID is an exact match
if k.KeyID == keyID && keyID != "" {
return k, nil
}
// keyIDs did not match or at least one was empty (if later, then it could be a match)
if k.KeyID == "" || keyID == "" {
validKeys = append(validKeys, k)
}
}
// if we get here, no match was possible at all (use / alg) or no exact match due to
// the signed JWT and / or the published keys didn't have a kid
// if later applies and only one key could be found, we'll return it
// otherwise a corresponding error will be thrown
if len(validKeys) == 1 {
return validKeys[0], nil
}
if len(validKeys) > 1 {
return key, ErrKeyMultiple
}
return key, ErrKeyNone
}
func algToKeyType(key any, alg string) bool {
switch alg[0] {
case 'R', 'P':
_, ok := key.(*rsa.PublicKey)
return ok
case 'E':
_, ok := key.(*ecdsa.PublicKey)
return ok
case 'O':
_, ok := key.(*ed25519.PublicKey)
return ok
default:
return false
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/keyset_test.go 0000664 0000000 0000000 00000016670 14656014552 0023313 0 ustar 00root root 0000000 0000000 package oidc
import (
"crypto/ecdsa"
"crypto/rsa"
"errors"
"reflect"
"testing"
jose "github.com/go-jose/go-jose/v4"
)
func TestFindKey(t *testing.T) {
type args struct {
keyID string
use string
expectedAlg string
keys []jose.JSONWebKey
}
type res struct {
key jose.JSONWebKey
err error
}
tests := []struct {
name string
args args
res res
}{
{
"no keys, ErrKeyNone",
args{
keyID: "",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: nil,
},
res{
key: jose.JSONWebKey{},
err: ErrKeyNone,
},
},
{
"single key enc, ErrKeyNone",
args{
keyID: "",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "enc",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{},
err: ErrKeyNone,
},
},
{
"single key wrong algorithm, ErrKeyNone",
args{
keyID: "",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
Key: &rsa.PrivateKey{},
},
},
},
res{
key: jose.JSONWebKey{},
err: ErrKeyNone,
},
},
{
"single key no kid, no jwt kid, match",
args{
keyID: "",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{
Use: "sig",
Key: &rsa.PublicKey{},
},
err: nil,
},
},
{
"single key kid, jwt no kid, match",
args{
keyID: "",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
KeyID: "id",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{
Use: "sig",
KeyID: "id",
Key: &rsa.PublicKey{},
},
err: nil,
},
},
{
"single key no kid, jwt with kid, match",
args{
keyID: "id",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{
Use: "sig",
Key: &rsa.PublicKey{},
},
err: nil,
},
},
{
"single key no use, jwt with kid, match",
args{
keyID: "id",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
KeyID: "id",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{
KeyID: "id",
Key: &rsa.PublicKey{},
},
err: nil,
},
},
{
"single key wrong kid, ErrKeyNone",
args{
keyID: "id",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
KeyID: "id2",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{},
err: ErrKeyNone,
},
},
{
"multiple keys no kid, jwt no kid, ErrKeyMultiple",
args{
keyID: "",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
Key: &rsa.PublicKey{},
},
{
Use: "sig",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{},
err: ErrKeyMultiple,
},
},
{
"multiple keys with kid, jwt no kid, ErrKeyMultiple",
args{
keyID: "",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
KeyID: "id1",
Key: &rsa.PublicKey{},
},
{
Use: "sig",
KeyID: "id2",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{},
err: ErrKeyMultiple,
},
},
{
"multiple keys, single sig key, jwt no kid, match",
args{
keyID: "",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
Key: &rsa.PublicKey{},
},
{
Use: "enc",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{
Use: "sig",
Key: &rsa.PublicKey{},
},
err: nil,
},
},
{
"multiple keys no kid, jwt with kid, ErrKeyMultiple",
args{
keyID: "id",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
Key: &rsa.PublicKey{},
},
{
Use: "sig",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{},
err: ErrKeyMultiple,
},
},
{
"multiple keys with kid, jwt with kid, match",
args{
keyID: "id1",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
KeyID: "id1",
Key: &rsa.PublicKey{},
},
{
Use: "sig",
KeyID: "id2",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{
Use: "sig",
KeyID: "id1",
Key: &rsa.PublicKey{},
},
err: nil,
},
},
{
"multiple keys, single sig key, jwt with kid, match",
args{
keyID: "id1",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
Use: "sig",
Key: &rsa.PublicKey{},
},
{
Use: "enc",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{
Use: "sig",
Key: &rsa.PublicKey{},
},
err: nil,
},
},
{
"multiple keys, no use, jwt with kid, match",
args{
keyID: "id1",
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
KeyID: "id1",
Key: &rsa.PublicKey{},
},
{
KeyID: "id2",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{
KeyID: "id1",
Key: &rsa.PublicKey{},
},
err: nil,
},
},
{
"multiple keys, no use, jwt without kid, ErrKeyMultiple",
args{
use: KeyUseSignature,
expectedAlg: "RS256",
keys: []jose.JSONWebKey{
{
KeyID: "id1",
Key: &rsa.PublicKey{},
},
{
KeyID: "id2",
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{},
err: ErrKeyMultiple,
},
},
{
"multiple keys, no use or id, jwt with kid, ErrKeyMultiple",
args{
use: KeyUseSignature,
expectedAlg: "RS256",
keyID: "id1",
keys: []jose.JSONWebKey{
{
Key: &rsa.PublicKey{},
},
{
Key: &rsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{},
err: ErrKeyMultiple,
},
},
{
"multiple keys (only one matching alg), jwt with kid, match",
args{
use: KeyUseSignature,
expectedAlg: "RS256",
keyID: "id1",
keys: []jose.JSONWebKey{
{
Key: &rsa.PublicKey{},
},
{
Key: &ecdsa.PublicKey{},
},
},
},
res{
key: jose.JSONWebKey{
Key: &rsa.PublicKey{},
},
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := FindMatchingKey(tt.args.keyID, tt.args.use, tt.args.expectedAlg, tt.args.keys...)
if (tt.res.err != nil && !errors.Is(err, tt.res.err)) || (tt.res.err == nil && err != nil) {
t.Errorf("FindKey() error, got = %v, want = %v", err, tt.res.err)
}
if !reflect.DeepEqual(got, tt.res.key) {
t.Errorf("FindKey() got = %v, want %v", got, tt.res.key)
}
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/regression_assert_test.go 0000664 0000000 0000000 00000002101 14656014552 0025530 0 ustar 00root root 0000000 0000000 //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))
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/regression_create_test.go 0000664 0000000 0000000 00000001020 14656014552 0025471 0 ustar 00root root 0000000 0000000 //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)
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/regression_data/ 0000775 0000000 0000000 00000000000 14656014552 0023560 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/oidc/regression_data/oidc.AccessTokenClaims.json 0000664 0000000 0000000 00000000475 14656014552 0030731 0 ustar 00root root 0000000 0000000 {
"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"
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/regression_data/oidc.IDTokenClaims.json 0000664 0000000 0000000 00000002146 14656014552 0030021 0 ustar 00root root 0000000 0000000 {
"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"
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/regression_data/oidc.IntrospectionResponse.json 0000664 0000000 0000000 00000002014 14656014552 0031744 0 ustar 00root root 0000000 0000000 {
"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"
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json 0000664 0000000 0000000 00000000172 14656014552 0032376 0 ustar 00root root 0000000 0000000 {
"aud": [
"foo",
"bar"
],
"exp": 12345,
"foo": "bar",
"iat": 12000,
"iss": "zitadel",
"sub": "hello@me.com"
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/regression_data/oidc.UserInfo.json 0000664 0000000 0000000 00000001453 14656014552 0027125 0 ustar 00root root 0000000 0000000 {
"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"
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/regression_test.go 0000664 0000000 0000000 00000001347 14656014552 0024162 0 ustar 00root root 0000000 0000000 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/.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,
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/revocation.go 0000664 0000000 0000000 00000000210 14656014552 0023100 0 ustar 00root root 0000000 0000000 package oidc
type RevocationRequest struct {
Token string `schema:"token"`
TokenTypeHint string `schema:"token_type_hint"`
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/session.go 0000664 0000000 0000000 00000000622 14656014552 0022421 0 ustar 00root root 0000000 0000000 package oidc
// EndSessionRequest for the RP-Initiated Logout according to:
//https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
type EndSessionRequest struct {
IdTokenHint string `schema:"id_token_hint"`
ClientID string `schema:"client_id"`
PostLogoutRedirectURI string `schema:"post_logout_redirect_uri"`
State string `schema:"state"`
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/token.go 0000664 0000000 0000000 00000027542 14656014552 0022070 0 ustar 00root root 0000000 0000000 package oidc
import (
"encoding/json"
"os"
"time"
jose "github.com/go-jose/go-jose/v4"
"golang.org/x/oauth2"
"github.com/muhlemmer/gu"
"github.com/zitadel/oidc/v3/pkg/crypto"
)
const (
// BearerToken defines the token_type `Bearer`, which is returned in a successful token response
BearerToken = "Bearer"
PrefixBearer = BearerToken + " "
)
type Tokens[C IDClaims] struct {
*oauth2.Token
IDTokenClaims C
IDToken string
}
// TokenClaims contains the base Claims used all tokens.
// It implements OpenID Connect Core 1.0, section 2.
// https://openid.net/specs/openid-connect-core-1_0.html#IDToken
// And RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens,
// section 2.2. https://datatracker.ietf.org/doc/html/rfc9068#name-data-structure
//
// 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 {
return c.Issuer
}
func (c *TokenClaims) GetSubject() string {
return c.Subject
}
func (c *TokenClaims) GetAudience() []string {
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)
if len(audience) == 0 {
audience = append(audience, clientID)
}
return &AccessTokenClaims{
TokenClaims: TokenClaims{
Issuer: issuer,
Subject: subject,
Audience: audience,
Expiration: FromTime(expiration),
IssuedAt: FromTime(now),
NotBefore: FromTime(now),
JWTID: jwtid,
},
}
}
type atcAlias AccessTokenClaims
func (a *AccessTokenClaims) MarshalJSON() ([]byte, error) {
return mergeAndMarshalClaims((*atcAlias)(a), a.Claims)
}
func (a *AccessTokenClaims) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*atcAlias)(a), &a.Claims)
}
// IDTokenClaims extends TokenClaims by further implementing
// OpenID Connect Core 1.0, sections 3.1.3.6 (Code flow),
// 3.2.2.10 (implicit), 3.3.2.11 (Hybrid) and 5.1 (UserInfo).
// https://openid.net/specs/openid-connect-core-1_0.html#toc
type IDTokenClaims struct {
TokenClaims
NotBefore Time `json:"nbf,omitempty"`
AccessTokenHash string `json:"at_hash,omitempty"`
CodeHash string `json:"c_hash,omitempty"`
SessionID string `json:"sid,omitempty"`
UserInfoProfile
UserInfoEmail
UserInfoPhone
Address *UserInfoAddress `json:"address,omitempty"`
Claims map[string]any `json:"-"`
}
// GetAccessTokenHash implements the IDTokenClaims interface
func (t *IDTokenClaims) GetAccessTokenHash() string {
return t.AccessTokenHash
}
func (t *IDTokenClaims) SetUserInfo(i *UserInfo) {
t.Subject = i.Subject
t.UserInfoProfile = i.UserInfoProfile
t.UserInfoEmail = i.UserInfoEmail
t.UserInfoPhone = i.UserInfoPhone
t.Address = i.Address
if t.Claims == nil {
t.Claims = make(map[string]any, len(t.Claims))
}
gu.MapMerge(i.Claims, t.Claims)
}
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),
}
}
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,
},
}
}
type itcAlias IDTokenClaims
func (i *IDTokenClaims) MarshalJSON() ([]byte, error) {
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 {
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
State string `json:"state,omitempty" schema:"state,omitempty"`
}
type JWTProfileAssertionClaims struct {
PrivateKeyID string `json:"-"`
PrivateKey []byte `json:"-"`
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience Audience `json:"aud"`
Expiration Time `json:"exp"`
IssuedAt Time `json:"iat"`
Claims map[string]any `json:"-"`
}
type jpaAlias JWTProfileAssertionClaims
func (j *JWTProfileAssertionClaims) MarshalJSON() ([]byte, error) {
return mergeAndMarshalClaims((*jpaAlias)(j), j.Claims)
}
func (j *JWTProfileAssertionClaims) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*jpaAlias)(j), &j.Claims)
}
func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (*JWTProfileAssertionClaims, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return NewJWTProfileAssertionFromFileData(data, audience, opts...)
}
func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string, opts ...AssertionOption) (string, error) {
keyData := new(struct {
KeyID string `json:"keyId"`
Key string `json:"key"`
UserID string `json:"userId"`
})
err := json.Unmarshal(data, keyData)
if err != nil {
return "", err
}
return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...))
}
func JWTProfileDelegatedSubject(sub string) func(*JWTProfileAssertionClaims) {
return func(j *JWTProfileAssertionClaims) {
j.Subject = sub
}
}
func JWTProfileCustomClaim(key string, value any) func(*JWTProfileAssertionClaims) {
return func(j *JWTProfileAssertionClaims) {
j.Claims[key] = value
}
}
func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (*JWTProfileAssertionClaims, error) {
keyData := new(struct {
KeyID string `json:"keyId"`
Key string `json:"key"`
UserID string `json:"userId"`
})
err := json.Unmarshal(data, keyData)
if err != nil {
return nil, err
}
return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...), nil
}
type AssertionOption func(*JWTProfileAssertionClaims)
func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) *JWTProfileAssertionClaims {
j := &JWTProfileAssertionClaims{
PrivateKey: key,
PrivateKeyID: keyID,
Issuer: userID,
Subject: userID,
IssuedAt: FromTime(time.Now().UTC()),
Expiration: FromTime(time.Now().Add(1 * time.Hour).UTC()),
Audience: audience,
Claims: make(map[string]any),
}
for _, opt := range opts {
opt(j)
}
return j
}
func ClaimHash(claim string, sigAlgorithm jose.SignatureAlgorithm) (string, error) {
hash, err := crypto.GetHashAlgorithm(sigAlgorithm)
if err != nil {
return "", err
}
return crypto.HashString(hash, claim, true), nil
}
func AppendClientIDToAudience(clientID string, audience []string) []string {
for _, aud := range audience {
if aud == clientID {
return audience
}
}
return append(audience, clientID)
}
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) {
privateKey, algorithm, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
if err != nil {
return "", err
}
key := jose.SigningKey{
Algorithm: algorithm,
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
}
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
if err != nil {
return "", err
}
marshalledAssertion, err := json.Marshal(assertion)
if err != nil {
return "", err
}
signedAssertion, err := signer.Sign(marshalledAssertion)
if err != nil {
return "", err
}
return signedAssertion.CompactSerialize()
}
type TokenExchangeResponse struct {
AccessToken string `json:"access_token"` // Can be access token or ID token
IssuedTokenType TokenType `json:"issued_token_type"`
TokenType string `json:"token_type"`
ExpiresIn uint64 `json:"expires_in,omitempty"`
Scopes SpaceDelimitedArray `json:"scope,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"`
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/token_request.go 0000664 0000000 0000000 00000017204 14656014552 0023632 0 ustar 00root root 0000000 0000000 package oidc
import (
"encoding/json"
"fmt"
"slices"
"time"
jose "github.com/go-jose/go-jose/v4"
)
const (
// GrantTypeCode defines the grant_type `authorization_code` used for the Token Request in the Authorization Code Flow
GrantTypeCode GrantType = "authorization_code"
// GrantTypeRefreshToken defines the grant_type `refresh_token` used for the Token Request in the Refresh Token Flow
GrantTypeRefreshToken GrantType = "refresh_token"
// GrantTypeClientCredentials defines the grant_type `client_credentials` used for the Token Request in the Client Credentials Token Flow
GrantTypeClientCredentials GrantType = "client_credentials"
// GrantTypeBearer defines the grant_type `urn:ietf:params:oauth:grant-type:jwt-bearer` used for the JWT Authorization Grant
GrantTypeBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
// GrantTypeTokenExchange defines the grant_type `urn:ietf:params:oauth:grant-type:token-exchange` used for the OAuth Token Exchange Grant
GrantTypeTokenExchange GrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
// GrantTypeImplicit defines the grant type `implicit` used for implicit flows that skip the generation and exchange of an Authorization Code
GrantTypeImplicit GrantType = "implicit"
// GrantTypeDeviceCode
GrantTypeDeviceCode GrantType = "urn:ietf:params:oauth:grant-type:device_code"
// ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`
// used for the OAuth JWT Profile Client Authentication
ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
)
var AllGrantTypes = []GrantType{
GrantTypeCode, GrantTypeRefreshToken, GrantTypeClientCredentials,
GrantTypeBearer, GrantTypeTokenExchange, GrantTypeImplicit,
GrantTypeDeviceCode, ClientAssertionTypeJWTAssertion,
}
type GrantType string
const (
AccessTokenType TokenType = "urn:ietf:params:oauth:token-type:access_token"
RefreshTokenType TokenType = "urn:ietf:params:oauth:token-type:refresh_token"
IDTokenType TokenType = "urn:ietf:params:oauth:token-type:id_token"
JWTTokenType TokenType = "urn:ietf:params:oauth:token-type:jwt"
)
var AllTokenTypes = []TokenType{
AccessTokenType, RefreshTokenType, IDTokenType, JWTTokenType,
}
type TokenType string
func (t TokenType) IsSupported() bool {
return slices.Contains(AllTokenTypes, t)
}
type TokenRequest interface {
// GrantType GrantType `schema:"grant_type"`
GrantType() GrantType
}
type TokenRequestType GrantType
type AccessTokenRequest struct {
Code string `schema:"code"`
RedirectURI string `schema:"redirect_uri"`
ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"`
CodeVerifier string `schema:"code_verifier"`
ClientAssertion string `schema:"client_assertion"`
ClientAssertionType string `schema:"client_assertion_type"`
}
func (a *AccessTokenRequest) GrantType() GrantType {
return GrantTypeCode
}
// SetClientID implements op.AuthenticatedTokenRequest
func (a *AccessTokenRequest) SetClientID(clientID string) {
a.ClientID = clientID
}
// SetClientSecret implements op.AuthenticatedTokenRequest
func (a *AccessTokenRequest) SetClientSecret(clientSecret string) {
a.ClientSecret = clientSecret
}
// RefreshTokenRequest is not useful for making refresh requests because the
// grant_type is not included explicitly but rather implied.
type RefreshTokenRequest struct {
RefreshToken string `schema:"refresh_token"`
Scopes SpaceDelimitedArray `schema:"scope"`
ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"`
ClientAssertion string `schema:"client_assertion"`
ClientAssertionType string `schema:"client_assertion_type"`
}
func (a *RefreshTokenRequest) GrantType() GrantType {
return GrantTypeRefreshToken
}
// SetClientID implements op.AuthenticatedTokenRequest
func (a *RefreshTokenRequest) SetClientID(clientID string) {
a.ClientID = clientID
}
// SetClientSecret implements op.AuthenticatedTokenRequest
func (a *RefreshTokenRequest) SetClientSecret(clientSecret string) {
a.ClientSecret = clientSecret
}
type JWTTokenRequest struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Scopes SpaceDelimitedArray `json:"-"`
Audience Audience `json:"aud"`
IssuedAt Time `json:"iat"`
ExpiresAt Time `json:"exp"`
private map[string]any
}
func (j *JWTTokenRequest) MarshalJSON() ([]byte, error) {
type Alias JWTTokenRequest
a := (*Alias)(j)
b, err := json.Marshal(a)
if err != nil {
return nil, err
}
if len(j.private) == 0 {
return b, nil
}
err = json.Unmarshal(b, &j.private)
if err != nil {
return nil, fmt.Errorf("jws: invalid map of custom claims %v", j.private)
}
return json.Marshal(j.private)
}
func (j *JWTTokenRequest) UnmarshalJSON(data []byte) error {
type Alias JWTTokenRequest
a := (*Alias)(j)
err := json.Unmarshal(data, a)
if err != nil {
return err
}
err = json.Unmarshal(data, &j.private)
if err != nil {
return err
}
return nil
}
func (j *JWTTokenRequest) GetCustomClaim(key string) any {
return j.private[key]
}
// GetIssuer implements the Claims interface
func (j *JWTTokenRequest) GetIssuer() string {
return j.Issuer
}
// GetAudience implements the Claims and TokenRequest interfaces
func (j *JWTTokenRequest) GetAudience() []string {
return j.Audience
}
// GetExpiration implements the Claims interface
func (j *JWTTokenRequest) GetExpiration() time.Time {
return j.ExpiresAt.AsTime()
}
// GetIssuedAt implements the Claims interface
func (j *JWTTokenRequest) GetIssuedAt() time.Time {
return j.IssuedAt.AsTime()
}
// GetNonce implements the Claims interface
func (j *JWTTokenRequest) GetNonce() string {
return ""
}
// GetAuthenticationContextClassReference implements the Claims interface
func (j *JWTTokenRequest) GetAuthenticationContextClassReference() string {
return ""
}
// GetAuthTime implements the Claims interface
func (j *JWTTokenRequest) GetAuthTime() time.Time {
return time.Time{}
}
// GetAuthorizedParty implements the Claims interface
func (j *JWTTokenRequest) GetAuthorizedParty() string {
return ""
}
// SetSignatureAlgorithm implements the Claims interface
func (j *JWTTokenRequest) SetSignatureAlgorithm(_ jose.SignatureAlgorithm) {}
// GetSubject implements the TokenRequest interface
func (j *JWTTokenRequest) GetSubject() string {
return j.Subject
}
// GetScopes implements the TokenRequest interface
func (j *JWTTokenRequest) GetScopes() []string {
return j.Scopes
}
type TokenExchangeRequest struct {
GrantType GrantType `schema:"grant_type"`
SubjectToken string `schema:"subject_token"`
SubjectTokenType TokenType `schema:"subject_token_type"`
ActorToken string `schema:"actor_token"`
ActorTokenType TokenType `schema:"actor_token_type"`
Resource []string `schema:"resource"`
Audience Audience `schema:"audience"`
Scopes SpaceDelimitedArray `schema:"scope"`
RequestedTokenType TokenType `schema:"requested_token_type"`
}
type ClientCredentialsRequest struct {
GrantType GrantType `schema:"grant_type,omitempty"`
Scope SpaceDelimitedArray `schema:"scope"`
ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"`
ClientAssertion string `schema:"client_assertion"`
ClientAssertionType string `schema:"client_assertion_type"`
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/token_test.go 0000664 0000000 0000000 00000016716 14656014552 0023130 0 ustar 00root root 0000000 0000000 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,
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)
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/types.go 0000664 0000000 0000000 00000014050 14656014552 0022102 0 ustar 00root root 0000000 0000000 package oidc
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"time"
jose "github.com/go-jose/go-jose/v4"
"github.com/muhlemmer/gu"
"github.com/zitadel/schema"
"golang.org/x/text/language"
)
type Audience []string
func (a *Audience) UnmarshalJSON(text []byte) error {
var i any
err := json.Unmarshal(text, &i)
if err != nil {
return err
}
switch aud := i.(type) {
case []any:
*a = make([]string, len(aud))
for i, audience := range aud {
(*a)[i] = audience.(string)
}
case string:
*a = []string{aud}
}
return nil
}
type Display string
func (d *Display) UnmarshalText(text []byte) error {
display := Display(text)
switch display {
case DisplayPage, DisplayPopup, DisplayTouch, DisplayWAP:
*d = display
}
return nil
}
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 {
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
// ParseLocales parses a slice of strings into Locales.
// If an entry causes a parse error or is undefined,
// it is ignored and not set to Locales.
func ParseLocales(locales []string) Locales {
out := make(Locales, 0, len(locales))
for _, locale := range locales {
tag, err := language.Parse(locale)
if err == nil && !tag.IsRoot() {
out = append(out, tag)
}
}
return out
}
// 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
}
type MaxAge *uint
func NewMaxAge(i uint) MaxAge {
return &i
}
type SpaceDelimitedArray []string
type Prompt SpaceDelimitedArray
type ResponseType string
type ResponseMode string
func (s SpaceDelimitedArray) String() string {
return strings.Join(s, " ")
}
func (s *SpaceDelimitedArray) UnmarshalText(text []byte) error {
*s = strings.Split(string(text), " ")
return nil
}
func (s SpaceDelimitedArray) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
func (s SpaceDelimitedArray) MarshalJSON() ([]byte, error) {
return json.Marshal((s).String())
}
func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
*s = strings.Split(str, " ")
return nil
}
func (s *SpaceDelimitedArray) Scan(src any) error {
if src == nil {
*s = nil
return nil
}
switch v := src.(type) {
case string:
if len(v) == 0 {
*s = SpaceDelimitedArray{}
return nil
}
*s = strings.Split(v, " ")
case []byte:
if len(v) == 0 {
*s = SpaceDelimitedArray{}
return nil
}
*s = strings.Split(string(v), " ")
default:
return fmt.Errorf("cannot convert %T to SpaceDelimitedArray", src)
}
return nil
}
func (s SpaceDelimitedArray) Value() (driver.Value, error) {
return strings.Join(s, " "), nil
}
// NewEncoder returns a schema Encoder with
// a registered encoder for SpaceDelimitedArray.
func NewEncoder() *schema.Encoder {
e := schema.NewEncoder()
e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string {
return value.Interface().(SpaceDelimitedArray).String()
})
return e
}
type Time int64
func (ts Time) AsTime() time.Time {
if ts == 0 {
return time.Time{}
}
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)
}
return nil
}
type RequestObject struct {
Issuer string `json:"iss"`
Audience Audience `json:"aud"`
AuthRequest
}
func (r *RequestObject) GetIssuer() string {
return r.Issuer
}
func (*RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/types_test.go 0000664 0000000 0000000 00000027655 14656014552 0023160 0 ustar 00root root 0000000 0000000 package oidc
import (
"bytes"
"encoding/json"
"net/url"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/schema"
"golang.org/x/text/language"
)
func TestAudience_UnmarshalText(t *testing.T) {
type args struct {
text []byte
}
type res struct {
audience Audience
}
tests := []struct {
name string
args args
res res
wantErr bool
}{
{
"invalid value",
args{
[]byte(`{"aud": {"a": }}}`),
},
res{},
true,
},
{
"single audience",
args{
[]byte(`{"aud": "single audience"}`),
},
res{
[]string{"single audience"},
},
false,
},
{
"multiple audience",
args{
[]byte(`{"aud": ["multiple", "audience"]}`),
},
res{
[]string{"multiple", "audience"},
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := new(struct {
Audience Audience `json:"aud"`
})
if err := json.Unmarshal(tt.args.text, &a); (err != nil) != tt.wantErr {
t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr)
}
assert.ElementsMatch(t, a.Audience, tt.res.audience)
})
}
}
func TestDisplay_UnmarshalText(t *testing.T) {
type args struct {
text []byte
}
type res struct {
display Display
}
tests := []struct {
name string
args args
res res
wantErr bool
}{
{
"unknown value",
args{
[]byte("unknown"),
},
res{},
false,
},
{
"page",
args{
[]byte("page"),
},
res{DisplayPage},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var d Display
if err := d.UnmarshalText(tt.args.text); (err != nil) != tt.wantErr {
t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr)
}
if d != tt.res.display {
t.Errorf("Display is not correct is = %v, want %v", d, tt.res.display)
}
})
}
}
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: "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 {
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) {
type args struct {
text []byte
}
type res struct {
tags []language.Tag
}
tests := []struct {
name string
args args
res res
wantErr bool
}{
{
"unknown value",
args{
[]byte("unknown"),
},
res{},
false,
},
{
"undefined",
args{
[]byte("und"),
},
res{},
false,
},
{
"single language",
args{
[]byte("de"),
},
res{[]language.Tag{language.German}},
false,
},
{
"multiple languages",
args{
[]byte("de en"),
},
res{[]language.Tag{language.German, language.English}},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var locales Locales
if err := locales.UnmarshalText(tt.args.text); (err != nil) != tt.wantErr {
t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr)
}
assert.ElementsMatch(t, locales, tt.res.tags)
})
}
}
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) {
type args struct {
text []byte
}
type res struct {
scopes []string
}
tests := []struct {
name string
args args
res res
wantErr bool
}{
{
"unknown value",
args{
[]byte("unknown"),
},
res{
[]string{"unknown"},
},
false,
},
{
"struct",
args{
[]byte(`{"unknown":"value"}`),
},
res{
[]string{`{"unknown":"value"}`},
},
false,
},
{
"openid",
args{
[]byte("openid"),
},
res{
[]string{"openid"},
},
false,
},
{
"multiple scopes",
args{
[]byte("openid email custom:scope"),
},
res{
[]string{"openid", "email", "custom:scope"},
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var scopes SpaceDelimitedArray
if err := scopes.UnmarshalText(tt.args.text); (err != nil) != tt.wantErr {
t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr)
}
assert.ElementsMatch(t, scopes, tt.res.scopes)
})
}
}
func TestScopes_MarshalText(t *testing.T) {
type args struct {
scopes SpaceDelimitedArray
}
type res struct {
scopes []byte
}
tests := []struct {
name string
args args
res res
wantErr bool
}{
{
"unknown value",
args{
SpaceDelimitedArray{"unknown"},
},
res{
[]byte("unknown"),
},
false,
},
{
"struct",
args{
SpaceDelimitedArray{`{"unknown":"value"}`},
},
res{
[]byte(`{"unknown":"value"}`),
},
false,
},
{
"openid",
args{
SpaceDelimitedArray{"openid"},
},
res{
[]byte("openid"),
},
false,
},
{
"multiple scopes",
args{
SpaceDelimitedArray{"openid", "email", "custom:scope"},
},
res{
[]byte("openid email custom:scope"),
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
text, err := tt.args.scopes.MarshalText()
if (err != nil) != tt.wantErr {
t.Errorf("MarshalText() error = %v, wantErr %v", err, tt.wantErr)
}
if !bytes.Equal(text, tt.res.scopes) {
t.Errorf("MarshalText() is = %q, want %q", text, tt.res.scopes)
}
})
}
}
func TestSpaceDelimitatedArray_ValuerNotNil(t *testing.T) {
inputs := [][]string{
{"two", "elements"},
{"one"},
{ /*zero*/ },
}
for _, input := range inputs {
t.Run(strconv.Itoa(len(input))+strings.Join(input, "_"), func(t *testing.T) {
sda := SpaceDelimitedArray(input)
dbValue, err := sda.Value()
if !assert.NoError(t, err, "Value") {
return
}
var reversed SpaceDelimitedArray
err = reversed.Scan(dbValue)
if assert.NoError(t, err, "Scan string") {
assert.Equal(t, sda, reversed, "scan string")
}
reversed = nil
dbValueString, ok := dbValue.(string)
if assert.True(t, ok, "dbValue is string") {
err = reversed.Scan([]byte(dbValueString))
if assert.NoError(t, err, "Scan bytes") {
assert.Equal(t, sda, reversed, "scan bytes")
}
}
})
}
}
func TestSpaceDelimitatedArray_ValuerNil(t *testing.T) {
var reversed SpaceDelimitedArray
err := reversed.Scan(nil)
if assert.NoError(t, err, "Scan nil") {
assert.Equal(t, SpaceDelimitedArray(nil), reversed, "scan nil")
}
}
func TestNewEncoder(t *testing.T) {
type request struct {
Scopes SpaceDelimitedArray `schema:"scope"`
}
a := request{
Scopes: SpaceDelimitedArray{"foo", "bar"},
}
values := make(url.Values)
NewEncoder().Encode(a, values)
assert.Equal(t, url.Values{"scope": []string{"foo bar"}}, values)
var b request
schema.NewDecoder().Decode(&b, values)
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)
})
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/userinfo.go 0000664 0000000 0000000 00000005566 14656014552 0022604 0 ustar 00root root 0000000 0000000 package oidc
// UserInfo implements OpenID Connect Core 1.0, section 5.1.
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims.
type UserInfo struct {
Subject string `json:"sub,omitempty"`
UserInfoProfile
UserInfoEmail
UserInfoPhone
Address *UserInfoAddress `json:"address,omitempty"`
Claims map[string]any `json:"-"`
}
func (u *UserInfo) AppendClaims(k string, v any) {
if u.Claims == nil {
u.Claims = make(map[string]any)
}
u.Claims[k] = v
}
// GetAddress is a safe getter that takes
// care of a possible nil value.
func (u *UserInfo) GetAddress() *UserInfoAddress {
if u.Address == nil {
return new(UserInfoAddress)
}
return u.Address
}
// GetSubject implements [rp.SubjectGetter]
func (u *UserInfo) GetSubject() string {
return u.Subject
}
type uiAlias UserInfo
func (u *UserInfo) MarshalJSON() ([]byte, error) {
return mergeAndMarshalClaims((*uiAlias)(u), u.Claims)
}
func (u *UserInfo) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*uiAlias)(u), &u.Claims)
}
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 *Locale `json:"locale,omitempty"`
UpdatedAt Time `json:"updated_at,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
}
type UserInfoEmail struct {
Email string `json:"email,omitempty"`
// Handle providers that return email_verified as a string
// https://forums.aws.amazon.com/thread.jspa?messageID=949441
// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
EmailVerified Bool `json:"email_verified,omitempty"`
}
type Bool bool
func (bs *Bool) UnmarshalJSON(data []byte) error {
if string(data) == "true" || string(data) == `"true"` {
*bs = true
}
return nil
}
type UserInfoPhone struct {
PhoneNumber string `json:"phone_number,omitempty"`
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
}
type UserInfoAddress struct {
Formatted string `json:"formatted,omitempty"`
StreetAddress string `json:"street_address,omitempty"`
Locality string `json:"locality,omitempty"`
Region string `json:"region,omitempty"`
PostalCode string `json:"postal_code,omitempty"`
Country string `json:"country,omitempty"`
}
type UserInfoRequest struct {
AccessToken string `schema:"access_token"`
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/userinfo_test.go 0000664 0000000 0000000 00000005433 14656014552 0023634 0 ustar 00root root 0000000 0000000 package oidc
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserInfo_AppendClaims(t *testing.T) {
u := new(UserInfo)
u.AppendClaims("a", "b")
want := map[string]any{"a": "b"}
assert.Equal(t, want, u.Claims)
u.AppendClaims("d", "e")
want["d"] = "e"
assert.Equal(t, want, u.Claims)
}
func TestUserInfo_GetAddress(t *testing.T) {
// nil address
u := new(UserInfo)
assert.Equal(t, &UserInfoAddress{}, u.GetAddress())
u.Address = &UserInfoAddress{PostalCode: "1234"}
assert.Equal(t, u.Address, u.GetAddress())
}
func TestUserInfoMarshal(t *testing.T) {
userinfo := &UserInfo{
Subject: "test",
Address: &UserInfoAddress{
StreetAddress: "Test 789\nPostfach 2",
},
UserInfoEmail: UserInfoEmail{
Email: "test",
EmailVerified: true,
},
UserInfoPhone: UserInfoPhone{
PhoneNumber: "0791234567",
PhoneNumberVerified: true,
},
UserInfoProfile: UserInfoProfile{
Name: "Test",
},
Claims: map[string]any{"private_claim": "test"},
}
marshal, err := json.Marshal(userinfo)
assert.NoError(t, err)
out := new(UserInfo)
assert.NoError(t, json.Unmarshal(marshal, out))
expected, err := json.Marshal(out)
assert.NoError(t, err)
assert.Equal(t, expected, marshal)
out2 := new(UserInfo)
assert.NoError(t, json.Unmarshal(expected, out2))
assert.Equal(t, out, out2)
}
func TestUserInfoEmailVerifiedUnmarshal(t *testing.T) {
t.Parallel()
t.Run("unmarshal email_verified from json bool true", func(t *testing.T) {
jsonBool := []byte(`{"email": "my@email.com", "email_verified": true}`)
var uie UserInfoEmail
err := json.Unmarshal(jsonBool, &uie)
assert.NoError(t, err)
assert.Equal(t, UserInfoEmail{
Email: "my@email.com",
EmailVerified: true,
}, uie)
})
t.Run("unmarshal email_verified from json string true", func(t *testing.T) {
jsonBool := []byte(`{"email": "my@email.com", "email_verified": "true"}`)
var uie UserInfoEmail
err := json.Unmarshal(jsonBool, &uie)
assert.NoError(t, err)
assert.Equal(t, UserInfoEmail{
Email: "my@email.com",
EmailVerified: true,
}, uie)
})
t.Run("unmarshal email_verified from json bool false", func(t *testing.T) {
jsonBool := []byte(`{"email": "my@email.com", "email_verified": false}`)
var uie UserInfoEmail
err := json.Unmarshal(jsonBool, &uie)
assert.NoError(t, err)
assert.Equal(t, UserInfoEmail{
Email: "my@email.com",
EmailVerified: false,
}, uie)
})
t.Run("unmarshal email_verified from json string false", func(t *testing.T) {
jsonBool := []byte(`{"email": "my@email.com", "email_verified": "false"}`)
var uie UserInfoEmail
err := json.Unmarshal(jsonBool, &uie)
assert.NoError(t, err)
assert.Equal(t, UserInfoEmail{
Email: "my@email.com",
EmailVerified: false,
}, uie)
})
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/util.go 0000664 0000000 0000000 00000003137 14656014552 0021717 0 ustar 00root root 0000000 0000000 package oidc
import (
"bytes"
"encoding/json"
"fmt"
)
// mergeAndMarshalClaims merges registered and the custom
// claims map into a single JSON object.
// Registered fields overwrite custom claims.
func mergeAndMarshalClaims(registered any, extraClaims map[string]any) ([]byte, error) {
// Use a buffer for memory re-use, instead off letting
// json allocate a new []byte for every step.
buf := new(bytes.Buffer)
// Marshal the registered claims into JSON
if err := json.NewEncoder(buf).Encode(registered); err != nil {
return nil, fmt.Errorf("oidc registered claims: %w", err)
}
if len(extraClaims) > 0 {
merged := make(map[string]any)
for k, v := range extraClaims {
merged[k] = v
}
// Merge JSON data into custom claims.
// The full-read action by the decoder resets the buffer
// to zero len, while retaining underlaying cap.
if err := json.NewDecoder(buf).Decode(&merged); err != nil {
return nil, fmt.Errorf("oidc registered claims: %w", err)
}
// Marshal the final result.
if err := json.NewEncoder(buf).Encode(merged); err != nil {
return nil, fmt.Errorf("oidc custom claims: %w", err)
}
}
return buf.Bytes(), nil
}
// unmarshalJSONMulti unmarshals the same JSON data into multiple destinations.
// Each destination must be a pointer, as per json.Unmarshal rules.
// Returns on the first error and destinations may be partly filled with data.
func unmarshalJSONMulti(data []byte, destinations ...any) error {
for _, dst := range destinations {
if err := json.Unmarshal(data, dst); err != nil {
return fmt.Errorf("oidc: %w into %T", err, dst)
}
}
return nil
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/util_test.go 0000664 0000000 0000000 00000005013 14656014552 0022751 0 ustar 00root root 0000000 0000000 package oidc
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type jsonErrorTest struct{}
func (jsonErrorTest) MarshalJSON() ([]byte, error) {
return nil, errors.New("test")
}
func Test_mergeAndMarshalClaims(t *testing.T) {
type args struct {
registered any
claims map[string]any
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "encoder error",
args: args{
registered: jsonErrorTest{},
},
wantErr: true,
},
{
name: "no claims",
args: args{
registered: struct {
Foo string `json:"foo,omitempty"`
}{
Foo: "bar",
},
},
want: "{\"foo\":\"bar\"}\n",
},
{
name: "with claims",
args: args{
registered: struct {
Foo string `json:"foo,omitempty"`
}{
Foo: "bar",
},
claims: map[string]any{
"bar": "foo",
},
},
want: "{\"bar\":\"foo\",\"foo\":\"bar\"}\n",
},
{
name: "registered overwrites custom",
args: args{
registered: struct {
Foo string `json:"foo,omitempty"`
}{
Foo: "bar",
},
claims: map[string]any{
"foo": "Hello, World!",
},
},
want: "{\"foo\":\"bar\"}\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := mergeAndMarshalClaims(tt.args.registered, tt.args.claims)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want, string(got))
})
}
}
func Test_unmarshalJSONMulti(t *testing.T) {
type dst struct {
Foo string `json:"foo,omitempty"`
}
type args struct {
data string
destinations []any
}
tests := []struct {
name string
args args
want []any
wantErr bool
}{
{
name: "error",
args: args{
data: "~!~~",
destinations: []any{
&dst{},
&map[string]any{},
},
},
want: []any{
&dst{},
&map[string]any{},
},
wantErr: true,
},
{
name: "success",
args: args{
data: "{\"bar\":\"foo\",\"foo\":\"bar\"}\n",
destinations: []any{
&dst{},
&map[string]any{},
},
},
want: []any{
&dst{Foo: "bar"},
&map[string]any{
"foo": "bar",
"bar": "foo",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := unmarshalJSONMulti([]byte(tt.args.data), tt.args.destinations...)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want, tt.args.destinations)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/verifier.go 0000664 0000000 0000000 00000017724 14656014552 0022564 0 ustar 00root root 0000000 0000000 package oidc
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
jose "github.com/go-jose/go-jose/v4"
str "github.com/zitadel/oidc/v3/pkg/strings"
)
type Claims interface {
GetIssuer() string
GetSubject() string
GetAudience() []string
GetExpiration() time.Time
GetIssuedAt() time.Time
GetNonce() string
GetAuthenticationContextClassReference() string
GetAuthTime() time.Time
GetAuthorizedParty() string
ClaimsSignature
}
type ClaimsSignature interface {
SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm)
}
type IDClaims interface {
Claims
GetSignatureAlgorithm() jose.SignatureAlgorithm
GetAccessTokenHash() string
}
var (
ErrParse = errors.New("parsing of request failed")
ErrIssuerInvalid = errors.New("issuer does not match")
ErrSubjectMissing = errors.New("subject missing")
ErrAudience = errors.New("audience is not valid")
ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty")
ErrAzpInvalid = errors.New("authorized party is not valid")
ErrSignatureMissing = errors.New("id_token does not contain a signature")
ErrSignatureMultiple = errors.New("id_token contains multiple signatures")
ErrSignatureUnsupportedAlg = errors.New("signature algorithm not supported")
ErrSignatureInvalidPayload = errors.New("signature does not match Payload")
ErrSignatureInvalid = errors.New("invalid signature")
ErrExpired = errors.New("token has expired")
ErrIatMissing = errors.New("issuedAt of token is missing")
ErrIatInFuture = errors.New("issuedAt of token is in the future")
ErrIatToOld = errors.New("issuedAt of token is to old")
ErrNonceInvalid = errors.New("nonce does not match")
ErrAcrInvalid = errors.New("acr is invalid")
ErrAuthTimeNotPresent = errors.New("claim `auth_time` of token is missing")
ErrAuthTimeToOld = errors.New("auth time of token is too old")
ErrAtHash = errors.New("at_hash does not correspond to access token")
)
// Verifier caries configuration for the various token verification
// functions. Use package specific constructor functions to know
// which values need to be set.
type Verifier struct {
Issuer string
MaxAgeIAT time.Duration
Offset time.Duration
ClientID string
SupportedSignAlgs []string
MaxAge time.Duration
ACR ACRVerifier
KeySet KeySet
Nonce func(ctx context.Context) string
}
// ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim
type ACRVerifier func(string) error
// DefaultACRVerifier implements `ACRVerifier` returning an error
// if none of the provided values matches the acr claim
func DefaultACRVerifier(possibleValues []string) ACRVerifier {
return func(acr string) error {
if !str.Contains(possibleValues, acr) {
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
}
return nil
}
}
func DecryptToken(tokenString string) (string, error) {
return tokenString, nil // TODO: impl
}
func ParseToken(tokenString string, claims any) ([]byte, error) {
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("%w: token contains an invalid number of segments", ErrParse)
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("%w: malformed jwt payload: %v", ErrParse, err)
}
err = json.Unmarshal(payload, claims)
return payload, err
}
func CheckSubject(claims Claims) error {
if claims.GetSubject() == "" {
return ErrSubjectMissing
}
return nil
}
func CheckIssuer(claims Claims, issuer string) error {
if claims.GetIssuer() != issuer {
return fmt.Errorf("%w: Expected: %s, got: %s", ErrIssuerInvalid, issuer, claims.GetIssuer())
}
return nil
}
func CheckAudience(claims Claims, clientID string) error {
if !str.Contains(claims.GetAudience(), clientID) {
return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID)
}
// TODO: check aud trusted
return nil
}
// CheckAuthorizedParty checks azp (authorized party) claim requirements.
//
// If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
// If an azp Claim is present, the Client SHOULD verify that its client_id is the Claim Value.
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func CheckAuthorizedParty(claims Claims, clientID string) error {
if len(claims.GetAudience()) > 1 {
if claims.GetAuthorizedParty() == "" {
return ErrAzpMissing
}
}
if claims.GetAuthorizedParty() != "" && claims.GetAuthorizedParty() != clientID {
return fmt.Errorf("%w: azp %q must be equal to client_id %q", ErrAzpInvalid, claims.GetAuthorizedParty(), clientID)
}
return nil
}
func CheckSignature(ctx context.Context, token string, payload []byte, claims ClaimsSignature, supportedSigAlgs []string, set KeySet) error {
jws, err := jose.ParseSigned(token, toJoseSignatureAlgorithms(supportedSigAlgs))
if err != nil {
if strings.HasPrefix(err.Error(), "go-jose/go-jose: unexpected signature algorithm") {
// TODO(v4): we should wrap errors instead of returning static ones.
// This is a workaround so we keep returning the same error for now.
return ErrSignatureUnsupportedAlg
}
return ErrParse
}
if len(jws.Signatures) == 0 {
return ErrSignatureMissing
}
if len(jws.Signatures) > 1 {
return ErrSignatureMultiple
}
sig := jws.Signatures[0]
signedPayload, err := set.VerifySignature(ctx, jws)
if err != nil {
return fmt.Errorf("%w (%v)", ErrSignatureInvalid, err)
}
if !bytes.Equal(signedPayload, payload) {
return ErrSignatureInvalidPayload
}
claims.SetSignatureAlgorithm(jose.SignatureAlgorithm(sig.Header.Algorithm))
return nil
}
// TODO(v4): Use the new jose.SignatureAlgorithm type directly, instead of string.
func toJoseSignatureAlgorithms(algorithms []string) []jose.SignatureAlgorithm {
out := make([]jose.SignatureAlgorithm, len(algorithms))
for i := range algorithms {
out[i] = jose.SignatureAlgorithm(algorithms[i])
}
if len(out) == 0 {
out = append(out, jose.RS256, jose.ES256, jose.PS256)
}
return out
}
func CheckExpiration(claims Claims, offset time.Duration) error {
expiration := claims.GetExpiration()
if !time.Now().Add(offset).Before(expiration) {
return ErrExpired
}
return nil
}
func CheckIssuedAt(claims Claims, maxAgeIAT, offset time.Duration) error {
issuedAt := claims.GetIssuedAt()
if issuedAt.IsZero() {
return ErrIatMissing
}
nowWithOffset := time.Now().Add(offset).Round(time.Second)
if issuedAt.After(nowWithOffset) {
return fmt.Errorf("%w: (iat: %v, now with offset: %v)", ErrIatInFuture, issuedAt, nowWithOffset)
}
if maxAgeIAT == 0 {
return nil
}
maxAge := time.Now().Add(-maxAgeIAT).Round(time.Second)
if issuedAt.Before(maxAge) {
return fmt.Errorf("%w: must not be older than %v, but was %v (%v to old)", ErrIatToOld, maxAge, issuedAt, maxAge.Sub(issuedAt))
}
return nil
}
func CheckNonce(claims Claims, nonce string) error {
if claims.GetNonce() != nonce {
return fmt.Errorf("%w: expected %q but was %q", ErrNonceInvalid, nonce, claims.GetNonce())
}
return nil
}
func CheckAuthorizationContextClassReference(claims Claims, acr ACRVerifier) error {
if acr != nil {
if err := acr(claims.GetAuthenticationContextClassReference()); err != nil {
return fmt.Errorf("%w: %v", ErrAcrInvalid, err)
}
}
return nil
}
func CheckAuthTime(claims Claims, maxAge time.Duration) error {
if maxAge == 0 {
return nil
}
if claims.GetAuthTime().IsZero() {
return ErrAuthTimeNotPresent
}
authTime := claims.GetAuthTime()
maxAuthTime := time.Now().Add(-maxAge).Round(time.Second)
if authTime.Before(maxAuthTime) {
return fmt.Errorf("%w: must not be older than %v, but was %v (%v to old)", ErrAuthTimeToOld, maxAge, authTime, maxAuthTime.Sub(authTime))
}
return nil
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/verifier_parse_test.go 0000664 0000000 0000000 00000005324 14656014552 0025006 0 ustar 00root root 0000000 0000000 package oidc_test
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tu "github.com/zitadel/oidc/v3/internal/testutil"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
func TestParseToken(t *testing.T) {
token, wantClaims := tu.ValidIDToken()
wantClaims.SignatureAlg = "" // unset, because is not part of the JSON payload
wantPayload, err := json.Marshal(wantClaims)
require.NoError(t, err)
tests := []struct {
name string
tokenString string
wantErr bool
}{
{
name: "split error",
tokenString: "nope",
wantErr: true,
},
{
name: "base64 error",
tokenString: "foo.~.bar",
wantErr: true,
},
{
name: "success",
tokenString: token,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotClaims := new(oidc.IDTokenClaims)
gotPayload, err := oidc.ParseToken(tt.tokenString, gotClaims)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, wantClaims, gotClaims)
assert.JSONEq(t, string(wantPayload), string(gotPayload))
})
}
}
func TestCheckSignature(t *testing.T) {
errCtx, cancel := context.WithCancel(context.Background())
cancel()
token, _ := tu.ValidIDToken()
payload, err := oidc.ParseToken(token, &oidc.IDTokenClaims{})
require.NoError(t, err)
type args struct {
ctx context.Context
token string
payload []byte
supportedSigAlgs []string
}
tests := []struct {
name string
args args
wantErr error
}{
{
name: "parse error",
args: args{
ctx: context.Background(),
token: "~",
payload: payload,
},
wantErr: oidc.ErrParse,
},
{
name: "default sigAlg",
args: args{
ctx: context.Background(),
token: token,
payload: payload,
},
},
{
name: "unsupported sigAlg",
args: args{
ctx: context.Background(),
token: token,
payload: payload,
supportedSigAlgs: []string{"foo", "bar"},
},
wantErr: oidc.ErrSignatureUnsupportedAlg,
},
{
name: "verify error",
args: args{
ctx: errCtx,
token: token,
payload: payload,
},
wantErr: oidc.ErrSignatureInvalid,
},
{
name: "inequal payloads",
args: args{
ctx: context.Background(),
token: token,
payload: []byte{0, 1, 2},
},
wantErr: oidc.ErrSignatureInvalidPayload,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
claims := new(oidc.TokenClaims)
err := oidc.CheckSignature(tt.args.ctx, tt.args.token, tt.args.payload, claims, tt.args.supportedSigAlgs, tu.KeySet{})
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/oidc/verifier_test.go 0000664 0000000 0000000 00000015371 14656014552 0023617 0 ustar 00root root 0000000 0000000 package oidc
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDecryptToken(t *testing.T) {
const tokenString = "ABC"
got, err := DecryptToken(tokenString)
require.NoError(t, err)
assert.Equal(t, tokenString, got)
}
func TestDefaultACRVerifier(t *testing.T) {
acrVerfier := DefaultACRVerifier([]string{"foo", "bar"})
tests := []struct {
name string
acr string
wantErr string
}{
{
name: "ok",
acr: "bar",
},
{
name: "error",
acr: "hello",
wantErr: "expected one of: [foo bar], got: \"hello\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := acrVerfier(tt.acr)
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
return
}
require.NoError(t, err)
})
}
}
func TestCheckSubject(t *testing.T) {
tests := []struct {
name string
claims Claims
wantErr error
}{
{
name: "missing",
claims: &TokenClaims{},
wantErr: ErrSubjectMissing,
},
{
name: "ok",
claims: &TokenClaims{
Subject: "foo",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CheckSubject(tt.claims)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCheckIssuer(t *testing.T) {
const issuer = "foo.bar"
tests := []struct {
name string
claims Claims
wantErr error
}{
{
name: "missing",
claims: &TokenClaims{},
wantErr: ErrIssuerInvalid,
},
{
name: "wrong",
claims: &TokenClaims{
Issuer: "wrong",
},
wantErr: ErrIssuerInvalid,
},
{
name: "ok",
claims: &TokenClaims{
Issuer: issuer,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CheckIssuer(tt.claims, issuer)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCheckAudience(t *testing.T) {
const clientID = "foo.bar"
tests := []struct {
name string
claims Claims
wantErr error
}{
{
name: "missing",
claims: &TokenClaims{},
wantErr: ErrAudience,
},
{
name: "wrong",
claims: &TokenClaims{
Audience: []string{"wrong"},
},
wantErr: ErrAudience,
},
{
name: "ok",
claims: &TokenClaims{
Audience: []string{clientID},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CheckAudience(tt.claims, clientID)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCheckAuthorizedParty(t *testing.T) {
const clientID = "foo.bar"
tests := []struct {
name string
claims Claims
wantErr error
}{
{
name: "single audience, no azp",
claims: &TokenClaims{
Audience: []string{clientID},
},
},
{
name: "multiple audience, no azp",
claims: &TokenClaims{
Audience: []string{clientID, "other"},
},
wantErr: ErrAzpMissing,
},
{
name: "single audience, with azp",
claims: &TokenClaims{
Audience: []string{clientID},
AuthorizedParty: clientID,
},
},
{
name: "multiple audience, with azp",
claims: &TokenClaims{
Audience: []string{clientID, "other"},
AuthorizedParty: clientID,
},
},
{
name: "wrong azp",
claims: &TokenClaims{
AuthorizedParty: "wrong",
},
wantErr: ErrAzpInvalid,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CheckAuthorizedParty(tt.claims, clientID)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCheckExpiration(t *testing.T) {
const offset = time.Minute
tests := []struct {
name string
claims Claims
wantErr error
}{
{
name: "missing",
claims: &TokenClaims{},
wantErr: ErrExpired,
},
{
name: "expired",
claims: &TokenClaims{
Expiration: FromTime(time.Now().Add(-2 * offset)),
},
wantErr: ErrExpired,
},
{
name: "valid",
claims: &TokenClaims{
Expiration: FromTime(time.Now().Add(2 * offset)),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CheckExpiration(tt.claims, offset)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCheckIssuedAt(t *testing.T) {
const offset = time.Minute
tests := []struct {
name string
maxAgeIAT time.Duration
claims Claims
wantErr error
}{
{
name: "missing",
claims: &TokenClaims{},
wantErr: ErrIatMissing,
},
{
name: "future",
claims: &TokenClaims{
IssuedAt: FromTime(time.Now().Add(time.Hour)),
},
wantErr: ErrIatInFuture,
},
{
name: "no max",
claims: &TokenClaims{
IssuedAt: FromTime(time.Now()),
},
},
{
name: "past max",
maxAgeIAT: time.Minute,
claims: &TokenClaims{
IssuedAt: FromTime(time.Now().Add(-time.Hour)),
},
wantErr: ErrIatToOld,
},
{
name: "within max",
maxAgeIAT: time.Hour,
claims: &TokenClaims{
IssuedAt: FromTime(time.Now()),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CheckIssuedAt(tt.claims, tt.maxAgeIAT, offset)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCheckNonce(t *testing.T) {
const nonce = "123"
tests := []struct {
name string
claims Claims
wantErr error
}{
{
name: "missing",
claims: &TokenClaims{},
wantErr: ErrNonceInvalid,
},
{
name: "wrong",
claims: &TokenClaims{
Nonce: "wrong",
},
wantErr: ErrNonceInvalid,
},
{
name: "ok",
claims: &TokenClaims{
Nonce: nonce,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CheckNonce(tt.claims, nonce)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCheckAuthorizationContextClassReference(t *testing.T) {
tests := []struct {
name string
acr ACRVerifier
wantErr error
}{
{
name: "error",
acr: func(s string) error { return errors.New("oops") },
wantErr: ErrAcrInvalid,
},
{
name: "ok",
acr: func(s string) error { return nil },
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CheckAuthorizationContextClassReference(&IDTokenClaims{}, tt.acr)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCheckAuthTime(t *testing.T) {
tests := []struct {
name string
claims Claims
maxAge time.Duration
wantErr error
}{
{
name: "no max age",
claims: &TokenClaims{},
},
{
name: "missing",
claims: &TokenClaims{},
maxAge: time.Minute,
wantErr: ErrAuthTimeNotPresent,
},
{
name: "expired",
maxAge: time.Minute,
claims: &TokenClaims{
AuthTime: FromTime(time.Now().Add(-time.Hour)),
},
wantErr: ErrAuthTimeToOld,
},
{
name: "ok",
maxAge: time.Minute,
claims: &TokenClaims{
AuthTime: NowTime(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CheckAuthTime(tt.claims, tt.maxAge)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/op/ 0000775 0000000 0000000 00000000000 14656014552 0020107 5 ustar 00root root 0000000 0000000 golang-github-zitadel-oidc-3.27.0/pkg/op/applicationtype_enumer.go 0000664 0000000 0000000 00000022532 14656014552 0025222 0 ustar 00root root 0000000 0000000 // Code generated by "enumer -linecomment -sql -json -text -yaml -gqlgen -type=ApplicationType,AccessTokenType"; DO NOT EDIT.
package op
import (
"database/sql/driver"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
)
const _ApplicationTypeName = "webuser_agentnative"
var _ApplicationTypeIndex = [...]uint8{0, 3, 13, 19}
const _ApplicationTypeLowerName = "webuser_agentnative"
func (i ApplicationType) String() string {
if i < 0 || i >= ApplicationType(len(_ApplicationTypeIndex)-1) {
return fmt.Sprintf("ApplicationType(%d)", i)
}
return _ApplicationTypeName[_ApplicationTypeIndex[i]:_ApplicationTypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _ApplicationTypeNoOp() {
var x [1]struct{}
_ = x[ApplicationTypeWeb-(0)]
_ = x[ApplicationTypeUserAgent-(1)]
_ = x[ApplicationTypeNative-(2)]
}
var _ApplicationTypeValues = []ApplicationType{ApplicationTypeWeb, ApplicationTypeUserAgent, ApplicationTypeNative}
var _ApplicationTypeNameToValueMap = map[string]ApplicationType{
_ApplicationTypeName[0:3]: ApplicationTypeWeb,
_ApplicationTypeLowerName[0:3]: ApplicationTypeWeb,
_ApplicationTypeName[3:13]: ApplicationTypeUserAgent,
_ApplicationTypeLowerName[3:13]: ApplicationTypeUserAgent,
_ApplicationTypeName[13:19]: ApplicationTypeNative,
_ApplicationTypeLowerName[13:19]: ApplicationTypeNative,
}
var _ApplicationTypeNames = []string{
_ApplicationTypeName[0:3],
_ApplicationTypeName[3:13],
_ApplicationTypeName[13:19],
}
// ApplicationTypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func ApplicationTypeString(s string) (ApplicationType, error) {
if val, ok := _ApplicationTypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _ApplicationTypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to ApplicationType values", s)
}
// ApplicationTypeValues returns all values of the enum
func ApplicationTypeValues() []ApplicationType {
return _ApplicationTypeValues
}
// ApplicationTypeStrings returns a slice of all String values of the enum
func ApplicationTypeStrings() []string {
strs := make([]string, len(_ApplicationTypeNames))
copy(strs, _ApplicationTypeNames)
return strs
}
// IsAApplicationType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i ApplicationType) IsAApplicationType() bool {
for _, v := range _ApplicationTypeValues {
if i == v {
return true
}
}
return false
}
// MarshalJSON implements the json.Marshaler interface for ApplicationType
func (i ApplicationType) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
// UnmarshalJSON implements the json.Unmarshaler interface for ApplicationType
func (i *ApplicationType) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("ApplicationType should be a string, got %s", data)
}
var err error
*i, err = ApplicationTypeString(s)
return err
}
// MarshalText implements the encoding.TextMarshaler interface for ApplicationType
func (i ApplicationType) MarshalText() ([]byte, error) {
return []byte(i.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface for ApplicationType
func (i *ApplicationType) UnmarshalText(text []byte) error {
var err error
*i, err = ApplicationTypeString(string(text))
return err
}
// MarshalYAML implements a YAML Marshaler for ApplicationType
func (i ApplicationType) MarshalYAML() (interface{}, error) {
return i.String(), nil
}
// UnmarshalYAML implements a YAML Unmarshaler for ApplicationType
func (i *ApplicationType) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
var err error
*i, err = ApplicationTypeString(s)
return err
}
func (i ApplicationType) Value() (driver.Value, error) {
return i.String(), nil
}
func (i *ApplicationType) Scan(value interface{}) error {
if value == nil {
return nil
}
var str string
switch v := value.(type) {
case []byte:
str = string(v)
case string:
str = v
case fmt.Stringer:
str = v.String()
default:
return fmt.Errorf("invalid value of ApplicationType: %[1]T(%[1]v)", value)
}
val, err := ApplicationTypeString(str)
if err != nil {
return err
}
*i = val
return nil
}
// MarshalGQL implements the graphql.Marshaler interface for ApplicationType
func (i ApplicationType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(i.String()))
}
// UnmarshalGQL implements the graphql.Unmarshaler interface for ApplicationType
func (i *ApplicationType) UnmarshalGQL(value interface{}) error {
str, ok := value.(string)
if !ok {
return fmt.Errorf("ApplicationType should be a string, got %T", value)
}
var err error
*i, err = ApplicationTypeString(str)
return err
}
const _AccessTokenTypeName = "bearerJWT"
var _AccessTokenTypeIndex = [...]uint8{0, 6, 9}
const _AccessTokenTypeLowerName = "bearerjwt"
func (i AccessTokenType) String() string {
if i < 0 || i >= AccessTokenType(len(_AccessTokenTypeIndex)-1) {
return fmt.Sprintf("AccessTokenType(%d)", i)
}
return _AccessTokenTypeName[_AccessTokenTypeIndex[i]:_AccessTokenTypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _AccessTokenTypeNoOp() {
var x [1]struct{}
_ = x[AccessTokenTypeBearer-(0)]
_ = x[AccessTokenTypeJWT-(1)]
}
var _AccessTokenTypeValues = []AccessTokenType{AccessTokenTypeBearer, AccessTokenTypeJWT}
var _AccessTokenTypeNameToValueMap = map[string]AccessTokenType{
_AccessTokenTypeName[0:6]: AccessTokenTypeBearer,
_AccessTokenTypeLowerName[0:6]: AccessTokenTypeBearer,
_AccessTokenTypeName[6:9]: AccessTokenTypeJWT,
_AccessTokenTypeLowerName[6:9]: AccessTokenTypeJWT,
}
var _AccessTokenTypeNames = []string{
_AccessTokenTypeName[0:6],
_AccessTokenTypeName[6:9],
}
// AccessTokenTypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func AccessTokenTypeString(s string) (AccessTokenType, error) {
if val, ok := _AccessTokenTypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _AccessTokenTypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to AccessTokenType values", s)
}
// AccessTokenTypeValues returns all values of the enum
func AccessTokenTypeValues() []AccessTokenType {
return _AccessTokenTypeValues
}
// AccessTokenTypeStrings returns a slice of all String values of the enum
func AccessTokenTypeStrings() []string {
strs := make([]string, len(_AccessTokenTypeNames))
copy(strs, _AccessTokenTypeNames)
return strs
}
// IsAAccessTokenType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i AccessTokenType) IsAAccessTokenType() bool {
for _, v := range _AccessTokenTypeValues {
if i == v {
return true
}
}
return false
}
// MarshalJSON implements the json.Marshaler interface for AccessTokenType
func (i AccessTokenType) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
// UnmarshalJSON implements the json.Unmarshaler interface for AccessTokenType
func (i *AccessTokenType) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("AccessTokenType should be a string, got %s", data)
}
var err error
*i, err = AccessTokenTypeString(s)
return err
}
// MarshalText implements the encoding.TextMarshaler interface for AccessTokenType
func (i AccessTokenType) MarshalText() ([]byte, error) {
return []byte(i.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface for AccessTokenType
func (i *AccessTokenType) UnmarshalText(text []byte) error {
var err error
*i, err = AccessTokenTypeString(string(text))
return err
}
// MarshalYAML implements a YAML Marshaler for AccessTokenType
func (i AccessTokenType) MarshalYAML() (interface{}, error) {
return i.String(), nil
}
// UnmarshalYAML implements a YAML Unmarshaler for AccessTokenType
func (i *AccessTokenType) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
var err error
*i, err = AccessTokenTypeString(s)
return err
}
func (i AccessTokenType) Value() (driver.Value, error) {
return i.String(), nil
}
func (i *AccessTokenType) Scan(value interface{}) error {
if value == nil {
return nil
}
var str string
switch v := value.(type) {
case []byte:
str = string(v)
case string:
str = v
case fmt.Stringer:
str = v.String()
default:
return fmt.Errorf("invalid value of AccessTokenType: %[1]T(%[1]v)", value)
}
val, err := AccessTokenTypeString(str)
if err != nil {
return err
}
*i = val
return nil
}
// MarshalGQL implements the graphql.Marshaler interface for AccessTokenType
func (i AccessTokenType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(i.String()))
}
// UnmarshalGQL implements the graphql.Unmarshaler interface for AccessTokenType
func (i *AccessTokenType) UnmarshalGQL(value interface{}) error {
str, ok := value.(string)
if !ok {
return fmt.Errorf("AccessTokenType should be a string, got %T", value)
}
var err error
*i, err = AccessTokenTypeString(str)
return err
}
golang-github-zitadel-oidc-3.27.0/pkg/op/auth_request.go 0000664 0000000 0000000 00000052670 14656014552 0023161 0 ustar 00root root 0000000 0000000 package op
import (
"bytes"
"context"
_ "embed"
"errors"
"fmt"
"html/template"
"log/slog"
"net"
"net/http"
"net/url"
"slices"
"strings"
"time"
"github.com/bmatcuk/doublestar/v4"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
str "github.com/zitadel/oidc/v3/pkg/strings"
)
type AuthRequest interface {
GetID() string
GetACR() string
GetAMR() []string
GetAudience() []string
GetAuthTime() time.Time
GetClientID() string
GetCodeChallenge() *oidc.CodeChallenge
GetNonce() string
GetRedirectURI() string
GetResponseType() oidc.ResponseType
GetResponseMode() oidc.ResponseMode
GetScopes() []string
GetState() string
GetSubject() string
Done() bool
}
type Authorizer interface {
Storage() Storage
Decoder() httphelper.Decoder
Encoder() httphelper.Encoder
IDTokenHintVerifier(context.Context) *IDTokenHintVerifier
Crypto() Crypto
RequestObjectSupported() bool
Logger() *slog.Logger
}
// AuthorizeValidator is an extension of Authorizer interface
// implementing its own validation mechanism for the auth request
type AuthorizeValidator interface {
Authorizer
ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, *IDTokenHintVerifier) (string, error)
}
func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
Authorize(w, r, authorizer)
}
}
func AuthorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
AuthorizeCallback(w, r, authorizer)
}
}
// Authorize handles the authorization request, including
// parsing, validating, storing and finally redirecting to the login handler
func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
ctx, span := tracer.Start(r.Context(), "Authorize")
r = r.WithContext(ctx)
defer span.End()
authReq, err := ParseAuthorizeRequest(r, authorizer.Decoder())
if err != nil {
AuthRequestError(w, r, nil, err, authorizer)
return
}
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
}
}
if authReq.ClientID == "" {
AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing client_id"), authorizer)
return
}
if authReq.RedirectURI == "" {
AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing redirect_uri"), authorizer)
return
}
validation := ValidateAuthRequest
if validater, ok := authorizer.(AuthorizeValidator); ok {
validation = validater.ValidateAuthRequest
}
userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx))
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
}
if authReq.RequestParam != "" {
AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer)
return
}
req, err := authorizer.Storage().CreateAuthRequest(ctx, authReq, userID)
if err != nil {
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer)
return
}
client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID())
if err != nil {
AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer)
return
}
RedirectToLogin(req.GetID(), client, w, r)
}
// ParseAuthorizeRequest parsed the http request into an oidc.AuthRequest
func ParseAuthorizeRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.AuthRequest, error) {
err := r.ParseForm()
if err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err)
}
authReq := new(oidc.AuthRequest)
err = decoder.Decode(authReq, r.Form)
if err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse auth request").WithParent(err)
}
return authReq, nil
}
// ParseRequestObject parse the `request` parameter, validates the token including the signature
// and copies the token claims into the auth request
func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, issuer string) error {
requestObject := new(oidc.RequestObject)
payload, err := oidc.ParseToken(authReq.RequestParam, requestObject)
if err != nil {
return err
}
if requestObject.ClientID != "" && requestObject.ClientID != authReq.ClientID {
return oidc.ErrInvalidRequest().WithDescription("missing or wrong client id in request")
}
if requestObject.ResponseType != "" && requestObject.ResponseType != authReq.ResponseType {
return oidc.ErrInvalidRequest().WithDescription("missing or wrong response type in request")
}
if requestObject.Issuer != requestObject.ClientID {
return oidc.ErrInvalidRequest().WithDescription("missing or wrong issuer in request")
}
if !str.Contains(requestObject.Audience, issuer) {
return oidc.ErrInvalidRequest().WithDescription("issuer missing in audience")
}
keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer}
if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil {
return oidc.ErrInvalidRequest().WithParent(err).WithDescription(err.Error())
}
CopyRequestObjectToAuthRequest(authReq, requestObject)
return nil
}
// CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request
// and clears the `RequestParam` of the auth request
func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) {
if str.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 {
authReq.Scopes = requestObject.Scopes
}
if requestObject.RedirectURI != "" {
authReq.RedirectURI = requestObject.RedirectURI
}
if requestObject.State != "" {
authReq.State = requestObject.State
}
if requestObject.ResponseMode != "" {
authReq.ResponseMode = requestObject.ResponseMode
}
if requestObject.Nonce != "" {
authReq.Nonce = requestObject.Nonce
}
if requestObject.Display != "" {
authReq.Display = requestObject.Display
}
if len(requestObject.Prompt) > 0 {
authReq.Prompt = requestObject.Prompt
}
if requestObject.MaxAge != nil {
authReq.MaxAge = requestObject.MaxAge
}
if len(requestObject.UILocales) > 0 {
authReq.UILocales = requestObject.UILocales
}
if requestObject.IDTokenHint != "" {
authReq.IDTokenHint = requestObject.IDTokenHint
}
if requestObject.LoginHint != "" {
authReq.LoginHint = requestObject.LoginHint
}
if len(requestObject.ACRValues) > 0 {
authReq.ACRValues = requestObject.ACRValues
}
if requestObject.CodeChallenge != "" {
authReq.CodeChallenge = requestObject.CodeChallenge
}
if requestObject.CodeChallengeMethod != "" {
authReq.CodeChallengeMethod = requestObject.CodeChallengeMethod
}
authReq.RequestParam = ""
}
// ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed
func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) {
ctx, span := tracer.Start(ctx, "ValidateAuthRequest")
defer span.End()
authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge)
if err != nil {
return "", err
}
client, err := storage.GetClientByClientID(ctx, authReq.ClientID)
if err != nil {
return "", oidc.DefaultToServerError(err, "unable to retrieve client by id")
}
authReq.Scopes, err = ValidateAuthReqScopes(client, authReq.Scopes)
if err != nil {
return "", err
}
if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil {
return "", err
}
if err := ValidateAuthReqResponseType(client, authReq.ResponseType); err != nil {
return "", err
}
return ValidateAuthReqIDTokenHint(ctx, authReq.IDTokenHint, verifier)
}
// ValidateAuthReqPrompt validates the passed prompt values and sets max_age to 0 if prompt login is present
func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error) {
for _, prompt := range prompts {
if prompt == oidc.PromptNone && len(prompts) > 1 {
return nil, oidc.ErrInvalidRequest().WithDescription("The prompt parameter `none` must only be used as a single value")
}
if prompt == oidc.PromptLogin {
maxAge = oidc.NewMaxAge(0)
}
}
return maxAge, nil
}
// ValidateAuthReqScopes validates the passed scopes and deletes any unsupported scopes.
// An error is returned if scopes is empty.
func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) {
if len(scopes) == 0 {
return nil, oidc.ErrInvalidRequest().
WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " +
"If you have any questions, you may contact the administrator of the application.")
}
scopes = slices.DeleteFunc(scopes, func(scope string) bool {
return !(scope == oidc.ScopeOpenID ||
scope == oidc.ScopeProfile ||
scope == oidc.ScopeEmail ||
scope == oidc.ScopePhone ||
scope == oidc.ScopeAddress ||
scope == oidc.ScopeOfflineAccess) &&
!client.IsScopeAllowed(scope)
})
return scopes, nil
}
// checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores
// other factors.
func checkURIAgainstRedirects(client Client, uri string) error {
if str.Contains(client.RedirectURIs(), uri) {
return nil
}
if globClient, ok := client.(HasRedirectGlobs); ok {
for _, uriGlob := range globClient.RedirectURIGlobs() {
isMatch, err := doublestar.Match(uriGlob, uri)
if err != nil {
return oidc.ErrServerError().WithParent(err)
}
if isMatch {
return nil
}
}
}
return oidc.ErrInvalidRequestRedirectURI().
WithDescription("The requested redirect_uri is missing in the client configuration. " +
"If you have any questions, you may contact the administrator of the application.")
}
// ValidateAuthReqRedirectURI validates the passed redirect_uri and response_type to the registered uris and client type
func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.ResponseType) error {
if uri == "" {
return oidc.ErrInvalidRequestRedirectURI().WithDescription("The redirect_uri is missing in the request. " +
"Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.")
}
if strings.HasPrefix(uri, "https://") {
return checkURIAgainstRedirects(client, uri)
}
if client.ApplicationType() == ApplicationTypeNative {
return validateAuthReqRedirectURINative(client, uri)
}
if err := checkURIAgainstRedirects(client, uri); err != nil {
return err
}
if strings.HasPrefix(uri, "http://") {
if client.DevMode() {
return nil
}
if responseType == oidc.ResponseTypeCode && IsConfidentialType(client) {
return nil
}
return oidc.ErrInvalidRequestRedirectURI().WithDescription("This client's redirect_uri is http and is not allowed. " +
"If you have any questions, you may contact the administrator of the application.")
}
return oidc.ErrInvalidRequestRedirectURI().WithDescription("This client's redirect_uri is using a custom schema and is not allowed. " +
"If you have any questions, you may contact the administrator of the application.")
}
// ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type
func validateAuthReqRedirectURINative(client Client, uri string) error {
parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri)
isCustomSchema := !strings.HasPrefix(uri, "http://")
if err := checkURIAgainstRedirects(client, uri); err == nil {
if client.DevMode() {
return nil
}
// The RedirectURIs are only valid for native clients when localhost or non-"http://"
if isLoopback || isCustomSchema {
return nil
}
return oidc.ErrInvalidRequestRedirectURI().WithDescription("This client's redirect_uri is http and is not allowed. " +
"If you have any questions, you may contact the administrator of the application.")
}
if !isLoopback {
return oidc.ErrInvalidRequestRedirectURI().WithDescription("The requested redirect_uri is missing in the client configuration. " +
"If you have any questions, you may contact the administrator of the application.")
}
for _, uri := range client.RedirectURIs() {
redirectURI, ok := HTTPLoopbackOrLocalhost(uri)
if ok && equalURI(parsedURL, redirectURI) {
return nil
}
}
return oidc.ErrInvalidRequestRedirectURI().WithDescription("The requested redirect_uri is missing in the client configuration." +
" If you have any questions, you may contact the administrator of the application.")
}
func equalURI(url1, url2 *url.URL) bool {
return url1.Path == url2.Path && url1.RawQuery == url2.RawQuery
}
func HTTPLoopbackOrLocalhost(rawURL string) (*url.URL, bool) {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return nil, false
}
if parsedURL.Scheme != "http" {
return nil, false
}
hostName := parsedURL.Hostname()
return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback()
}
// ValidateAuthReqResponseType validates the passed response_type to the registered response types
func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType) error {
if responseType == "" {
return oidc.ErrInvalidRequest().WithDescription("The response type is missing in your request. " +
"If you have any questions, you may contact the administrator of the application.")
}
if !ContainsResponseType(client.ResponseTypes(), responseType) {
return oidc.ErrUnauthorizedClient().WithDescription("The requested response type is missing in the client configuration. " +
"If you have any questions, you may contact the administrator of the application.")
}
return nil
}
// ValidateAuthReqIDTokenHint validates the id_token_hint (if passed as parameter in the request)
// and returns the `sub` claim
func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifier *IDTokenHintVerifier) (string, error) {
if idTokenHint == "" {
return "", nil
}
claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, idTokenHint, verifier)
if err != nil && !errors.As(err, &IDTokenHintExpiredError{}) {
return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " +
"If you have any questions, you may contact the administrator of the application.").WithParent(err)
}
return claims.GetSubject(), nil
}
// RedirectToLogin redirects the end user to the Login UI for authentication
func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r *http.Request) {
login := client.LoginURL(authReqID)
http.Redirect(w, r, login, http.StatusFound)
}
// AuthorizeCallback handles the callback after authentication in the Login UI
func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
ctx, span := tracer.Start(r.Context(), "AuthorizeCallback")
r = r.WithContext(ctx)
defer span.End()
id, err := ParseAuthorizeCallbackRequest(r)
if err != nil {
AuthRequestError(w, r, nil, err, authorizer)
return
}
authReq, err := authorizer.Storage().AuthRequestByID(r.Context(), id)
if err != nil {
AuthRequestError(w, r, nil, err, authorizer)
return
}
if !authReq.Done() {
AuthRequestError(w, r, authReq,
oidc.ErrInteractionRequired().WithDescription("Unfortunately, the user may be not logged in and/or additional interaction is required."),
authorizer)
return
}
AuthResponse(authReq, authorizer, w, r)
}
func ParseAuthorizeCallbackRequest(r *http.Request) (id string, err error) {
if err = r.ParseForm(); err != nil {
return "", fmt.Errorf("cannot parse form: %w", err)
}
id = r.Form.Get("id")
if id == "" {
return "", errors.New("auth request callback is missing id")
}
return id, nil
}
// AuthResponse creates the successful authentication response (either code or tokens)
func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "AuthResponse")
r = r.WithContext(ctx)
defer span.End()
client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID())
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
}
if authReq.GetResponseType() == oidc.ResponseTypeCode {
AuthResponseCode(w, r, authReq, authorizer)
return
}
AuthResponseToken(w, r, authReq, authorizer, client)
}
// AuthResponseCode creates the successful code authentication response
func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) {
ctx, span := tracer.Start(r.Context(), "AuthResponseCode")
r = r.WithContext(ctx)
defer span.End()
code, err := CreateAuthRequestCode(r.Context(), authReq, authorizer.Storage(), authorizer.Crypto())
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
}
codeResponse := struct {
Code string `schema:"code"`
State string `schema:"state,omitempty"`
}{
Code: code,
State: authReq.GetState(),
}
if authReq.GetResponseMode() == oidc.ResponseModeFormPost {
err := AuthResponseFormPost(w, authReq.GetRedirectURI(), &codeResponse, authorizer.Encoder())
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
}
return
}
callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder())
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
}
http.Redirect(w, r, callback, http.StatusFound)
}
// AuthResponseToken creates the successful token(s) authentication response
func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer, client Client) {
ctx, span := tracer.Start(r.Context(), "AuthResponseToken")
defer span.End()
r = r.WithContext(ctx)
createAccessToken := authReq.GetResponseType() != oidc.ResponseTypeIDTokenOnly
resp, err := CreateTokenResponse(r.Context(), authReq, client, authorizer, createAccessToken, "", "")
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
}
if authReq.GetResponseMode() == oidc.ResponseModeFormPost {
err := AuthResponseFormPost(w, authReq.GetRedirectURI(), resp, authorizer.Encoder())
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
}
return
}
callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), resp, authorizer.Encoder())
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
}
http.Redirect(w, r, callback, http.StatusFound)
}
// CreateAuthRequestCode creates and stores a code for the auth code response
func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Storage, crypto Crypto) (string, error) {
ctx, span := tracer.Start(ctx, "CreateAuthRequestCode")
defer span.End()
code, err := BuildAuthRequestCode(authReq, crypto)
if err != nil {
return "", err
}
if err := storage.SaveAuthCode(ctx, authReq.GetID(), code); err != nil {
return "", err
}
return code, nil
}
// BuildAuthRequestCode builds the string representation of the auth code
func BuildAuthRequestCode(authReq AuthRequest, crypto Crypto) (string, error) {
return crypto.Encrypt(authReq.GetID())
}
// AuthResponseURL encodes the authorization response (successful and error) and sets it as query or fragment values
// depending on the response_mode and response_type
func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, responseMode oidc.ResponseMode, response any, encoder httphelper.Encoder) (string, error) {
uri, err := url.Parse(redirectURI)
if err != nil {
return "", oidc.ErrServerError().WithParent(err)
}
params, err := httphelper.URLEncodeParams(response, encoder)
if err != nil {
return "", oidc.ErrServerError().WithParent(err)
}
// return explicitly requested mode
if responseMode == oidc.ResponseModeQuery {
return mergeQueryParams(uri, params), nil
}
if responseMode == oidc.ResponseModeFragment {
return setFragment(uri, params), nil
}
// implicit must use fragment mode is not specified by client
if responseType == oidc.ResponseTypeIDToken || responseType == oidc.ResponseTypeIDTokenOnly {
return setFragment(uri, params), nil
}
// if we get here it's code flow: defaults to query
return mergeQueryParams(uri, params), nil
}
//go:embed form_post.html.tmpl
var formPostHtmlTemplate string
var formPostTmpl = template.Must(template.New("form_post").Parse(formPostHtmlTemplate))
// AuthResponseFormPost responds a html page that automatically submits the form which contains the auth response parameters
func AuthResponseFormPost(res http.ResponseWriter, redirectURI string, response any, encoder httphelper.Encoder) error {
values := make(map[string][]string)
err := encoder.Encode(response, values)
if err != nil {
return oidc.ErrServerError().WithParent(err)
}
params := &struct {
RedirectURI string
Params any
}{
RedirectURI: redirectURI,
Params: values,
}
var buf bytes.Buffer
err = formPostTmpl.Execute(&buf, params)
if err != nil {
return oidc.ErrServerError().WithParent(err)
}
res.Header().Set("Cache-Control", "no-store")
res.WriteHeader(http.StatusOK)
_, err = buf.WriteTo(res)
if err != nil {
return oidc.ErrServerError().WithParent(err)
}
return nil
}
func setFragment(uri *url.URL, params url.Values) string {
uri.Fragment = params.Encode()
return uri.String()
}
func mergeQueryParams(uri *url.URL, params url.Values) string {
queries := uri.Query()
for param, values := range params {
for _, value := range values {
queries.Add(param, value)
}
}
uri.RawQuery = queries.Encode()
return uri.String()
}
golang-github-zitadel-oidc-3.27.0/pkg/op/auth_request_test.go 0000664 0000000 0000000 00000067633 14656014552 0024225 0 ustar 00root root 0000000 0000000 package op_test
import (
"context"
"errors"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/example/server/storage"
tu "github.com/zitadel/oidc/v3/internal/testutil"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/oidc/v3/pkg/op/mock"
"github.com/zitadel/schema"
)
func TestAuthorize(t *testing.T) {
tests := []struct {
name string
req *http.Request
expect func(a *mock.MockAuthorizerMockRecorder)
}{
{
name: "parse error", // used to panic, see issue #315
req: httptest.NewRequest(http.MethodPost, "/?;", nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
authorizer := mock.NewMockAuthorizer(gomock.NewController(t))
expect := authorizer.EXPECT()
expect.Decoder().Return(schema.NewDecoder())
expect.Logger().Return(slog.Default())
if tt.expect != nil {
tt.expect(expect)
}
op.Authorize(w, tt.req, authorizer)
})
}
}
func TestParseAuthorizeRequest(t *testing.T) {
type args struct {
r *http.Request
decoder httphelper.Decoder
}
type res struct {
want *oidc.AuthRequest
err bool
}
tests := []struct {
name string
args args
res res
}{
{
"parsing form error",
args{
&http.Request{URL: &url.URL{RawQuery: "invalid=%%param"}},
schema.NewDecoder(),
},
res{
nil,
true,
},
},
{
"decoding error",
args{
&http.Request{URL: &url.URL{RawQuery: "unknown=value"}},
func() httphelper.Decoder {
decoder := schema.NewDecoder()
decoder.IgnoreUnknownKeys(false)
return decoder
}(),
},
res{
nil,
true,
},
},
{
"parsing ok",
args{
&http.Request{URL: &url.URL{RawQuery: "scope=openid"}},
func() httphelper.Decoder {
decoder := schema.NewDecoder()
decoder.IgnoreUnknownKeys(false)
return decoder
}(),
},
res{
&oidc.AuthRequest{Scopes: oidc.SpaceDelimitedArray{"openid"}},
false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := op.ParseAuthorizeRequest(tt.args.r, tt.args.decoder)
if (err != nil) != tt.res.err {
t.Errorf("ParseAuthorizeRequest() error = %v, wantErr %v", err, tt.res.err)
}
if !reflect.DeepEqual(got, tt.res.want) {
t.Errorf("ParseAuthorizeRequest() got = %v, want %v", got, tt.res.want)
}
})
}
}
func TestValidateAuthRequest(t *testing.T) {
type args struct {
authRequest *oidc.AuthRequest
storage op.Storage
verifier *op.IDTokenHintVerifier
}
tests := []struct {
name string
args args
wantErr error
}{
{
"scope missing fails",
args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil},
oidc.ErrInvalidRequest(),
},
{
"response_type missing fails",
args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil},
oidc.ErrInvalidRequest(),
},
{
"client_id missing fails",
args{&oidc.AuthRequest{Scopes: []string{"openid"}, ResponseType: oidc.ResponseTypeCode}, mock.NewMockStorageExpectValidClientID(t), nil},
oidc.ErrInvalidRequest(),
},
{
"redirect_uri missing fails",
args{&oidc.AuthRequest{Scopes: []string{"openid"}, ResponseType: oidc.ResponseTypeCode, ClientID: "client_id"}, mock.NewMockStorageExpectValidClientID(t), nil},
oidc.ErrInvalidRequest(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := op.ValidateAuthRequest(context.TODO(), tt.args.authRequest, tt.args.storage, tt.args.verifier)
if tt.wantErr == nil && err != nil {
t.Errorf("ValidateAuthRequest() unexpected error = %v", err)
}
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
t.Errorf("ValidateAuthRequest() unexpected error = %v, want = %v", err, tt.wantErr)
}
})
}
}
func TestValidateAuthReqPrompt(t *testing.T) {
type args struct {
prompts []string
maxAge *uint
}
type res struct {
maxAge *uint
err error
}
tests := []struct {
name string
args args
res res
}{
{
"no prompts and maxAge, ok",
args{
nil,
nil,
},
res{
nil,
nil,
},
},
{
"no prompts but maxAge, ok",
args{
nil,
oidc.NewMaxAge(10),
},
res{
oidc.NewMaxAge(10),
nil,
},
},
{
"prompt none, ok",
args{
[]string{"none"},
oidc.NewMaxAge(10),
},
res{
oidc.NewMaxAge(10),
nil,
},
},
{
"prompt none with others, err",
args{
[]string{"none", "login"},
oidc.NewMaxAge(10),
},
res{
nil,
oidc.ErrInvalidRequest(),
},
},
{
"prompt login, ok",
args{
[]string{"login"},
nil,
},
res{
oidc.NewMaxAge(0),
nil,
},
},
{
"prompt login with maxAge, ok",
args{
[]string{"login"},
oidc.NewMaxAge(10),
},
res{
oidc.NewMaxAge(0),
nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
maxAge, err := op.ValidateAuthReqPrompt(tt.args.prompts, tt.args.maxAge)
if tt.res.err == nil && err != nil {
t.Errorf("ValidateAuthRequest() unexpected error = %v", err)
}
if tt.res.err != nil && !errors.Is(err, tt.res.err) {
t.Errorf("ValidateAuthRequest() unexpected error = %v, want = %v", err, tt.res.err)
}
assert.Equal(t, tt.res.maxAge, maxAge)
})
}
}
func TestValidateAuthReqScopes(t *testing.T) {
type args struct {
client op.Client
scopes []string
}
type res struct {
err bool
scopes []string
}
tests := []struct {
name string
args args
res res
}{
{
"scopes missing fails",
args{},
res{
err: true,
},
},
{
"scope ok",
args{
mock.NewClientExpectAny(t, op.ApplicationTypeWeb),
[]string{"openid"},
},
res{
scopes: []string{"openid"},
},
},
{
"scope with drop ok",
args{
mock.NewClientExpectAny(t, op.ApplicationTypeWeb),
[]string{"openid", "email", "unknown"},
},
res{
scopes: []string{"openid", "email"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scopes, err := op.ValidateAuthReqScopes(tt.args.client, tt.args.scopes)
if (err != nil) != tt.res.err {
t.Errorf("ValidateAuthReqScopes() error = %v, wantErr %v", err, tt.res.err)
}
assert.ElementsMatch(t, scopes, tt.res.scopes)
})
}
}
func TestValidateAuthReqRedirectURI(t *testing.T) {
type args struct {
uri string
client op.Client
responseType oidc.ResponseType
}
tests := []struct {
name string
args args
wantErr bool
}{
{
"empty fails",
args{
"",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeWeb, nil, false),
oidc.ResponseTypeCode,
},
true,
},
{
"unregistered https fails",
args{
"https://unregistered.com/callback",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeWeb, nil, false),
oidc.ResponseTypeCode,
},
true,
},
{
"unregistered http fails",
args{
"http://unregistered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeWeb, nil, false),
oidc.ResponseTypeCode,
},
true,
},
{
"code flow registered https web ok",
args{
"https://registered.com/callback",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeWeb, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow registered https native ok",
args{
"https://registered.com/callback",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow registered https user agent ok",
args{
"https://registered.com/callback",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow registered http confidential (web) ok",
args{
"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeWeb, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow registered http not confidential (native) fails",
args{
"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
true,
},
{
"code flow registered http not confidential (user agent) fails",
args{
"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeCode,
},
true,
},
{
"code flow registered http localhost native ok",
args{
"http://localhost:4200/callback",
mock.NewClientWithConfig(t, []string{"http://localhost/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow registered http loopback v4 native ok",
args{
"http://127.0.0.1:4200/callback",
mock.NewClientWithConfig(t, []string{"http://127.0.0.1/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow registered http loopback v6 native ok",
args{
"http://[::1]:4200/callback",
mock.NewClientWithConfig(t, []string{"http://[::1]/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow unregistered http native fails",
args{
"http://unregistered.com/callback",
mock.NewClientWithConfig(t, []string{"http://locahost/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
true,
},
{
"code flow unregistered custom native fails",
args{
"unregistered://callback",
mock.NewClientWithConfig(t, []string{"registered://callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
true,
},
{
"code flow unregistered loopback native fails",
args{
"http://[::1]:4200/unregistered",
mock.NewClientWithConfig(t, []string{"http://[::1]:4200/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
true,
},
{
"code flow registered custom not native (web) fails",
args{
"custom://callback",
mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeWeb, nil, false),
oidc.ResponseTypeCode,
},
true,
},
{
"code flow registered custom not native (user agent) fails",
args{
"custom://callback",
mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeCode,
},
true,
},
{
"code flow registered custom native ok",
args{
"custom://callback",
mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode http ok",
args{
"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"implicit flow registered ok",
args{
"https://registered.com/callback",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeIDToken,
},
false,
},
{
"implicit flow unregistered fails",
args{
"https://unregistered.com/callback",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeIDToken,
},
true,
},
{
"implicit flow registered http localhost native ok",
args{
"http://localhost:9999/callback",
mock.NewClientWithConfig(t, []string{"http://localhost:9999/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeIDToken,
},
false,
},
{
"implicit flow registered http localhost web fails",
args{
"http://localhost:9999/callback",
mock.NewClientWithConfig(t, []string{"http://localhost:9999/callback"}, op.ApplicationTypeWeb, nil, false),
oidc.ResponseTypeIDToken,
},
true,
},
{
"implicit flow registered http localhost user agent fails",
args{
"http://localhost:9999/callback",
mock.NewClientWithConfig(t, []string{"http://localhost:9999/callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeIDToken,
},
true,
},
{
"implicit flow http non localhost fails",
args{
"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeIDToken,
},
true,
},
{
"implicit flow custom fails",
args{
"custom://callback",
mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeIDToken,
},
false,
},
{
"implicit flow dev mode http ok",
args{
"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeIDToken,
},
false,
},
{
"code flow dev mode has redirect globs regular ok",
args{
"http://registered.com/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://registered.com/*"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode has redirect globs wildcard ok",
args{
"http://registered.com/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://registered.com/*"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode has redirect globs double star ok",
args{
"http://registered.com/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://**/*"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode has redirect globs double star ok",
args{
"http://registered.com/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://**/*"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode has redirect globs IPv6 ok",
args{
"http://[::1]:80/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://\\[::1\\]:80/*"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode has redirect globs bad pattern",
args{
"http://registered.com/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://**/\\"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := op.ValidateAuthReqRedirectURI(tt.args.client, tt.args.uri, tt.args.responseType); (err != nil) != tt.wantErr {
t.Errorf("ValidateRedirectURI() error = %v, wantErr %v", err.Error(), tt.wantErr)
}
})
}
}
func TestLoopbackOrLocalhost(t *testing.T) {
type args struct {
url string
}
tests := []struct {
name string
args args
want bool
}{
{
"not parsable, false",
args{url: string('\n')},
false,
},
{
"not http, false",
args{url: "localhost/test"},
false,
},
{
"not http, false",
args{url: "http://localhost.com/test"},
false,
},
{
"v4 no port ok",
args{url: "http://127.0.0.1/test"},
true,
},
{
"v6 short no port ok",
args{url: "http://[::1]/test"},
true,
},
{
"v6 long no port ok",
args{url: "http://[0:0:0:0:0:0:0:1]/test"},
true,
},
{
"locahost no port ok",
args{url: "http://localhost/test"},
true,
},
{
"v4 with port ok",
args{url: "http://127.0.0.1:4200/test"},
true,
},
{
"v6 short with port ok",
args{url: "http://[::1]:4200/test"},
true,
},
{
"v6 long with port ok",
args{url: "http://[0:0:0:0:0:0:0:1]:4200/test"},
true,
},
{
"localhost with port ok",
args{url: "http://localhost:4200/test"},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, got := op.HTTPLoopbackOrLocalhost(tt.args.url); got != tt.want {
t.Errorf("loopbackOrLocalhost() = %v, want %v", got, tt.want)
}
})
}
}
func TestValidateAuthReqResponseType(t *testing.T) {
type args struct {
responseType oidc.ResponseType
client op.Client
}
tests := []struct {
name string
args args
wantErr bool
}{
{
"empty response type",
args{
"",
mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true),
},
true,
},
{
"response type missing in client config",
args{
oidc.ResponseTypeIDToken,
mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true),
},
true,
},
{
"valid response type",
args{
oidc.ResponseTypeCode,
mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true),
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := op.ValidateAuthReqResponseType(tt.args.client, tt.args.responseType); (err != nil) != tt.wantErr {
t.Errorf("ValidateAuthReqScopes() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestRedirectToLogin(t *testing.T) {
type args struct {
authReqID string
client op.Client
w http.ResponseWriter
r *http.Request
}
tests := []struct {
name string
args args
}{
{
"redirect ok",
args{
"id",
mock.NewClientExpectAny(t, op.ApplicationTypeNative),
httptest.NewRecorder(),
httptest.NewRequest("GET", "/authorize", nil),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
op.RedirectToLogin(tt.args.authReqID, tt.args.client, tt.args.w, tt.args.r)
rec := tt.args.w.(*httptest.ResponseRecorder)
require.Equal(t, http.StatusFound, rec.Code)
require.Equal(t, "/login?id=id", rec.Header().Get("location"))
})
}
}
func TestAuthResponseURL(t *testing.T) {
type args struct {
redirectURI string
responseType oidc.ResponseType
responseMode oidc.ResponseMode
response any
encoder httphelper.Encoder
}
type res struct {
url string
err error
}
tests := []struct {
name string
args args
res res
}{
{
"encoding error",
args{
"uri",
oidc.ResponseTypeCode,
"",
map[string]any{"test": "test"},
&mockEncoder{
errors.New("error encoding"),
},
},
res{
"",
oidc.ErrServerError(),
},
},
{
"response mode query",
args{
"uri",
oidc.ResponseTypeIDToken,
oidc.ResponseModeQuery,
map[string][]string{"test": {"test"}},
&mockEncoder{},
},
res{
"uri?test=test",
nil,
},
},
{
"response mode fragment",
args{
"uri",
oidc.ResponseTypeCode,
oidc.ResponseModeFragment,
map[string][]string{"test": {"test"}},
&mockEncoder{},
},
res{
"uri#test=test",
nil,
},
},
{
"response type code",
args{
"uri",
oidc.ResponseTypeCode,
"",
map[string][]string{"test": {"test"}},
&mockEncoder{},
},
res{
"uri?test=test",
nil,
},
},
{
"response type id token",
args{
"uri",
oidc.ResponseTypeIDToken,
"",
map[string][]string{"test": {"test"}},
&mockEncoder{},
},
res{
"uri#test=test",
nil,
},
},
{
"with query",
args{
"uri?param=value",
oidc.ResponseTypeCode,
"",
map[string][]string{"test": {"test"}},
&mockEncoder{},
},
res{
"uri?param=value&test=test",
nil,
},
},
{
"with query response type id token",
args{
"uri?param=value",
oidc.ResponseTypeIDToken,
"",
map[string][]string{"test": {"test"}},
&mockEncoder{},
},
res{
"uri?param=value#test=test",
nil,
},
},
{
"with existing query",
args{
"uri?test=value",
oidc.ResponseTypeCode,
"",
map[string][]string{"test": {"test"}},
&mockEncoder{},
},
res{
"uri?test=value&test=test",
nil,
},
},
{
"with existing query response type id token",
args{
"uri?test=value",
oidc.ResponseTypeIDToken,
"",
map[string][]string{"test": {"test"}},
&mockEncoder{},
},
res{
"uri?test=value#test=test",
nil,
},
},
{
"with existing query and multiple values",
args{
"uri?test=value",
oidc.ResponseTypeCode,
"",
map[string][]string{"test": {"test", "test2"}},
&mockEncoder{},
},
res{
"uri?test=value&test=test&test=test2",
nil,
},
},
{
"with existing query and multiple values response type id token",
args{
"uri?test=value",
oidc.ResponseTypeIDToken,
"",
map[string][]string{"test": {"test", "test2"}},
&mockEncoder{},
},
res{
"uri?test=value#test=test&test=test2",
nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := op.AuthResponseURL(tt.args.redirectURI, tt.args.responseType, tt.args.responseMode, tt.args.response, tt.args.encoder)
if tt.res.err == nil && err != nil {
t.Errorf("ValidateAuthRequest() unexpected error = %v", err)
}
if tt.res.err != nil && !errors.Is(err, tt.res.err) {
t.Errorf("ValidateAuthRequest() unexpected error = %v, want = %v", err, tt.res.err)
}
if got != tt.res.url {
t.Errorf("AuthResponseURL() got = %v, want %v", got, tt.res.url)
}
})
}
}
type mockEncoder struct {
err error
}
func (m *mockEncoder) Encode(src any, dst map[string][]string) error {
if m.err != nil {
return m.err
}
for s, strings := range src.(map[string][]string) {
dst[s] = strings
}
return nil
}
// mockCrypto implements the op.Crypto interface
// and in always equals out. (It doesn't crypt anything).
// When returnErr != nil, that error is always returned instread.
type mockCrypto struct {
returnErr error
}
func (c *mockCrypto) Encrypt(s string) (string, error) {
if c.returnErr != nil {
return "", c.returnErr
}
return s, nil
}
func (c *mockCrypto) Decrypt(s string) (string, error) {
if c.returnErr != nil {
return "", c.returnErr
}
return s, nil
}
func TestAuthResponseCode(t *testing.T) {
type args struct {
authReq op.AuthRequest
authorizer func(*testing.T) op.Authorizer
}
type res struct {
wantCode int
wantLocationHeader string
wantCacheControlHeader string
wantBody string
}
tests := []struct {
name string
args args
res res
}{
{
name: "create code error",
args: args{
authReq: &storage.AuthRequest{
ID: "id1",
TransferState: "state1",
},
authorizer: func(t *testing.T) op.Authorizer {
ctrl := gomock.NewController(t)
storage := mock.NewMockStorage(ctrl)
authorizer := mock.NewMockAuthorizer(ctrl)
authorizer.EXPECT().Storage().Return(storage)
authorizer.EXPECT().Crypto().Return(&mockCrypto{
returnErr: io.ErrClosedPipe,
})
authorizer.EXPECT().Logger().Return(slog.Default())
return authorizer
},
},
res: res{
wantCode: http.StatusBadRequest,
wantBody: "io: read/write on closed pipe\n",
},
},
{
name: "success with state",
args: args{
authReq: &storage.AuthRequest{
ID: "id1",
TransferState: "state1",
},
authorizer: func(t *testing.T) op.Authorizer {
ctrl := gomock.NewController(t)
storage := mock.NewMockStorage(ctrl)
storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1")
authorizer := mock.NewMockAuthorizer(ctrl)
authorizer.EXPECT().Storage().Return(storage)
authorizer.EXPECT().Crypto().Return(&mockCrypto{})
authorizer.EXPECT().Encoder().Return(schema.NewEncoder())
return authorizer
},
},
res: res{
wantCode: http.StatusFound,
wantLocationHeader: "/auth/callback/?code=id1&state=state1",
wantBody: "",
},
},
{
name: "success without state", // reproduce issue #415
args: args{
authReq: &storage.AuthRequest{
ID: "id1",
TransferState: "",
},
authorizer: func(t *testing.T) op.Authorizer {
ctrl := gomock.NewController(t)
storage := mock.NewMockStorage(ctrl)
storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1")
authorizer := mock.NewMockAuthorizer(ctrl)
authorizer.EXPECT().Storage().Return(storage)
authorizer.EXPECT().Crypto().Return(&mockCrypto{})
authorizer.EXPECT().Encoder().Return(schema.NewEncoder())
return authorizer
},
},
res: res{
wantCode: http.StatusFound,
wantLocationHeader: "/auth/callback/?code=id1",
wantBody: "",
},
},
{
name: "success form_post",
args: args{
authReq: &storage.AuthRequest{
ID: "id1",
CallbackURI: "https://example.com/callback",
TransferState: "state1",
ResponseMode: "form_post",
},
authorizer: func(t *testing.T) op.Authorizer {
ctrl := gomock.NewController(t)
storage := mock.NewMockStorage(ctrl)
storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1")
authorizer := mock.NewMockAuthorizer(ctrl)
authorizer.EXPECT().Storage().Return(storage)
authorizer.EXPECT().Crypto().Return(&mockCrypto{})
authorizer.EXPECT().Encoder().Return(schema.NewEncoder())
return authorizer
},
},
res: res{
wantCode: http.StatusOK,
wantCacheControlHeader: "no-store",
wantBody: "\n\n\n\n\n\n",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/auth/callback/", nil)
w := httptest.NewRecorder()
op.AuthResponseCode(w, r, tt.args.authReq, tt.args.authorizer(t))
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, tt.res.wantCode, resp.StatusCode)
assert.Equal(t, tt.res.wantLocationHeader, resp.Header.Get("Location"))
assert.Equal(t, tt.res.wantCacheControlHeader, resp.Header.Get("Cache-Control"))
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, tt.res.wantBody, string(body))
})
}
}
func Test_parseAuthorizeCallbackRequest(t *testing.T) {
tests := []struct {
name string
url string
wantId string
wantErr bool
}{
{
name: "parse error",
url: "/?id;=99",
wantErr: true,
},
{
name: "missing id",
url: "/",
wantErr: true,
},
{
name: "ok",
url: "/?id=99",
wantId: "99",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, tt.url, nil)
gotId, err := op.ParseAuthorizeCallbackRequest(r)
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.wantId, gotId)
})
}
}
func TestValidateAuthReqIDTokenHint(t *testing.T) {
token, _ := tu.ValidIDToken()
tests := []struct {
name string
idTokenHint string
want string
wantErr error
}{
{
name: "empty",
},
{
name: "verify err",
idTokenHint: "foo",
wantErr: oidc.ErrLoginRequired(),
},
{
name: "ok",
idTokenHint: token,
want: tu.ValidSubject,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := op.ValidateAuthReqIDTokenHint(context.Background(), tt.idTokenHint, op.NewIDTokenHintVerifier(tu.ValidIssuer, tu.KeySet{}))
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/op/client.go 0000664 0000000 0000000 00000014702 14656014552 0021720 0 ustar 00root root 0000000 0000000 package op
import (
"context"
"errors"
"net/http"
"net/url"
"time"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
//go:generate go get github.com/dmarkham/enumer
//go:generate go run github.com/dmarkham/enumer -linecomment -sql -json -text -yaml -gqlgen -type=ApplicationType,AccessTokenType
//go:generate go mod tidy
const (
ApplicationTypeWeb ApplicationType = iota // web
ApplicationTypeUserAgent // user_agent
ApplicationTypeNative // native
)
const (
AccessTokenTypeBearer AccessTokenType = iota // bearer
AccessTokenTypeJWT // JWT
)
type ApplicationType int
type AuthMethod string
type AccessTokenType int
type Client interface {
GetID() string
RedirectURIs() []string
PostLogoutRedirectURIs() []string
ApplicationType() ApplicationType
AuthMethod() oidc.AuthMethod
ResponseTypes() []oidc.ResponseType
GrantTypes() []oidc.GrantType
LoginURL(string) string
AccessTokenType() AccessTokenType
IDTokenLifetime() time.Duration
DevMode() bool
RestrictAdditionalIdTokenScopes() func(scopes []string) []string
RestrictAdditionalAccessTokenScopes() func(scopes []string) []string
IsScopeAllowed(scope string) bool
IDTokenUserinfoClaimsAssertion() bool
ClockSkew() time.Duration
}
// HasRedirectGlobs is an optional interface that can be implemented by implementors of
// Client. See https://pkg.go.dev/path#Match for glob
// interpretation. Redirect URIs that match either the non-glob version or the
// glob version will be accepted. Glob URIs are only partially supported for native
// clients: "http://" is not allowed except for loopback or in dev mode.
//
// Note that globbing / wildcards are not permitted by the OIDC
// standard and implementing this interface can have security implications.
// It is advised to only return a client of this type in rare cases,
// such as DevMode for the client being enabled.
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
type HasRedirectGlobs interface {
Client
RedirectURIGlobs() []string
PostLogoutRedirectURIGlobs() []string
}
func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool {
for _, t := range types {
if t == responseType {
return true
}
}
return false
}
func IsConfidentialType(c Client) bool {
return c.ApplicationType() == ApplicationTypeWeb
}
var (
ErrInvalidAuthHeader = errors.New("invalid basic auth header")
ErrNoClientCredentials = errors.New("no client credentials provided")
ErrMissingClientID = errors.New("client_id missing from request")
)
type ClientJWTProfile interface {
JWTProfileVerifier(context.Context) *JWTProfileVerifier
}
func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) {
ctx, span := tracer.Start(ctx, "ClientJWTAuth")
defer span.End()
if ca.ClientAssertion == "" {
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
}
profile, err := VerifyJWTAssertion(ctx, ca.ClientAssertion, verifier.JWTProfileVerifier(ctx))
if err != nil {
return "", oidc.ErrUnauthorizedClient().WithParent(err).WithDescription("JWT assertion failed")
}
return profile.Issuer, nil
}
func ClientBasicAuth(r *http.Request, storage Storage) (clientID string, err error) {
ctx, span := tracer.Start(r.Context(), "ClientBasicAuth")
r = r.WithContext(ctx)
defer span.End()
clientID, clientSecret, ok := r.BasicAuth()
if !ok {
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
}
clientID, err = url.QueryUnescape(clientID)
if err != nil {
return "", oidc.ErrInvalidClient().WithParent(ErrInvalidAuthHeader)
}
clientSecret, err = url.QueryUnescape(clientSecret)
if err != nil {
return "", oidc.ErrInvalidClient().WithParent(ErrInvalidAuthHeader)
}
if err := storage.AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil {
return "", oidc.ErrUnauthorizedClient().WithParent(err)
}
return clientID, nil
}
type ClientProvider interface {
Decoder() httphelper.Decoder
Storage() Storage
}
type clientData struct {
ClientID string `schema:"client_id"`
oidc.ClientAssertionParams
}
// ClientIDFromRequest parses the request form and tries to obtain the client ID
// and reports if it is authenticated, using a JWT or static client secrets over
// http basic auth.
//
// If the Provider implements IntrospectorJWTProfile and "client_assertion" is
// present in the form data, JWT assertion will be verified and the
// client ID is taken from there.
// If any of them is absent, basic auth is attempted.
// In absence of basic auth data, the unauthenticated client id from the form
// data is returned.
//
// If no client id can be obtained by any method, oidc.ErrInvalidClient
// is returned with ErrMissingClientID wrapped in it.
func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, authenticated bool, err error) {
err = r.ParseForm()
if err != nil {
return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err)
}
ctx, span := tracer.Start(r.Context(), "ClientIDFromRequest")
r = r.WithContext(ctx)
defer span.End()
data := new(clientData)
if err = p.Decoder().Decode(data, r.Form); err != nil {
return "", false, err
}
JWTProfile, ok := p.(ClientJWTProfile)
if ok && data.ClientAssertion != "" {
// if JWTProfile is supported and client sent an assertion, check it and use it as response
// regardless if it succeeded or failed
clientID, err = ClientJWTAuth(r.Context(), data.ClientAssertionParams, JWTProfile)
return clientID, err == nil, err
}
// try basic auth
clientID, err = ClientBasicAuth(r, p.Storage())
// if that succeeded, use it
if err == nil {
return clientID, true, nil
}
// if the client did not send a Basic Auth Header, ignore the `ErrNoClientCredentials`
// but return other errors immediately
if !errors.Is(err, ErrNoClientCredentials) {
return "", false, err
}
// if the client did not authenticate (public clients) it must at least send a client_id
if data.ClientID == "" {
return "", false, oidc.ErrInvalidClient().WithParent(ErrMissingClientID)
}
return data.ClientID, false, nil
}
type ClientCredentials struct {
ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"` // Client secret from Basic auth or request body
ClientAssertion string `schema:"client_assertion"` // JWT
ClientAssertionType string `schema:"client_assertion_type"`
}
golang-github-zitadel-oidc-3.27.0/pkg/op/client_test.go 0000664 0000000 0000000 00000012503 14656014552 0022754 0 ustar 00root root 0000000 0000000 package op_test
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/oidc/v3/pkg/op/mock"
"github.com/zitadel/schema"
)
type testClientJWTProfile struct{}
func (testClientJWTProfile) JWTProfileVerifier(context.Context) *op.JWTProfileVerifier { return nil }
func TestClientJWTAuth(t *testing.T) {
type args struct {
ctx context.Context
ca oidc.ClientAssertionParams
verifier op.ClientJWTProfile
}
tests := []struct {
name string
args args
wantClientID string
wantErr error
}{
{
name: "empty assertion",
args: args{
context.Background(),
oidc.ClientAssertionParams{},
testClientJWTProfile{},
},
wantErr: op.ErrNoClientCredentials,
},
{
name: "verification error",
args: args{
context.Background(),
oidc.ClientAssertionParams{
ClientAssertion: "foo",
},
testClientJWTProfile{},
},
wantErr: oidc.ErrParse,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotClientID, err := op.ClientJWTAuth(tt.args.ctx, tt.args.ca, tt.args.verifier)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.wantClientID, gotClientID)
})
}
}
func TestClientBasicAuth(t *testing.T) {
errWrong := errors.New("wrong secret")
type args struct {
username string
password string
}
tests := []struct {
name string
args *args
storage op.Storage
wantClientID string
wantErr error
}{
{
name: "no args",
wantErr: op.ErrNoClientCredentials,
},
{
name: "username unescape err",
args: &args{
username: "%",
password: "bar",
},
wantErr: op.ErrInvalidAuthHeader,
},
{
name: "password unescape err",
args: &args{
username: "foo",
password: "%",
},
wantErr: op.ErrInvalidAuthHeader,
},
{
name: "auth error",
args: &args{
username: "foo",
password: "wrong",
},
storage: func() op.Storage {
s := mock.NewMockStorage(gomock.NewController(t))
s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "wrong").Return(errWrong)
return s
}(),
wantErr: errWrong,
},
{
name: "auth error",
args: &args{
username: "foo",
password: "bar",
},
storage: func() op.Storage {
s := mock.NewMockStorage(gomock.NewController(t))
s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "bar").Return(nil)
return s
}(),
wantClientID: "foo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/foo", nil)
if tt.args != nil {
r.SetBasicAuth(tt.args.username, tt.args.password)
}
gotClientID, err := op.ClientBasicAuth(r, tt.storage)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.wantClientID, gotClientID)
})
}
}
type errReader struct{}
func (errReader) Read([]byte) (int, error) {
return 0, io.ErrNoProgress
}
type testClientProvider struct {
storage op.Storage
}
func (testClientProvider) Decoder() httphelper.Decoder {
return schema.NewDecoder()
}
func (p testClientProvider) Storage() op.Storage {
return p.storage
}
func TestClientIDFromRequest(t *testing.T) {
type args struct {
body io.Reader
p op.ClientProvider
}
type basicAuth struct {
username string
password string
}
tests := []struct {
name string
args args
basicAuth *basicAuth
wantClientID string
wantAuthenticated bool
wantErr bool
}{
{
name: "parse error",
args: args{
body: errReader{},
},
wantErr: true,
},
{
name: "unauthenticated",
args: args{
body: strings.NewReader(
url.Values{
"client_id": []string{"foo"},
}.Encode(),
),
p: testClientProvider{
storage: mock.NewStorage(t),
},
},
wantClientID: "foo",
wantAuthenticated: false,
},
{
name: "authenticated",
args: args{
body: strings.NewReader(
url.Values{}.Encode(),
),
p: testClientProvider{
storage: func() op.Storage {
s := mock.NewMockStorage(gomock.NewController(t))
s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "bar").Return(nil)
return s
}(),
},
},
basicAuth: &basicAuth{
username: "foo",
password: "bar",
},
wantClientID: "foo",
wantAuthenticated: true,
},
{
name: "missing client id",
args: args{
body: strings.NewReader(
url.Values{}.Encode(),
),
p: testClientProvider{
storage: mock.NewStorage(t),
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/foo", tt.args.body)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if tt.basicAuth != nil {
r.SetBasicAuth(tt.basicAuth.username, tt.basicAuth.password)
}
gotClientID, gotAuthenticated, err := op.ClientIDFromRequest(r, tt.args.p)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.wantClientID, gotClientID)
assert.Equal(t, tt.wantAuthenticated, gotAuthenticated)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/op/config.go 0000664 0000000 0000000 00000012007 14656014552 0021703 0 ustar 00root root 0000000 0000000 package op
import (
"errors"
"log"
"net/http"
"net/url"
"strings"
"github.com/muhlemmer/httpforwarded"
"golang.org/x/text/language"
)
var (
ErrInvalidIssuerPath = errors.New("no fragments or query allowed for issuer")
ErrInvalidIssuerNoIssuer = errors.New("missing issuer")
ErrInvalidIssuerURL = errors.New("invalid url for issuer")
ErrInvalidIssuerMissingHost = errors.New("host for issuer missing")
ErrInvalidIssuerHTTPS = errors.New("scheme for issuer must be `https`")
)
type Configuration interface {
IssuerFromRequest(r *http.Request) string
Insecure() bool
AuthorizationEndpoint() *Endpoint
TokenEndpoint() *Endpoint
IntrospectionEndpoint() *Endpoint
UserinfoEndpoint() *Endpoint
RevocationEndpoint() *Endpoint
EndSessionEndpoint() *Endpoint
KeysEndpoint() *Endpoint
DeviceAuthorizationEndpoint() *Endpoint
AuthMethodPostSupported() bool
CodeMethodS256Supported() bool
AuthMethodPrivateKeyJWTSupported() bool
TokenEndpointSigningAlgorithmsSupported() []string
GrantTypeRefreshTokenSupported() bool
GrantTypeTokenExchangeSupported() bool
GrantTypeJWTAuthorizationSupported() bool
GrantTypeClientCredentialsSupported() bool
GrantTypeDeviceCodeSupported() bool
IntrospectionAuthMethodPrivateKeyJWTSupported() bool
IntrospectionEndpointSigningAlgorithmsSupported() []string
RevocationAuthMethodPrivateKeyJWTSupported() bool
RevocationEndpointSigningAlgorithmsSupported() []string
RequestObjectSupported() bool
RequestObjectSigningAlgorithmsSupported() []string
SupportedUILocales() []language.Tag
DeviceAuthorization() DeviceAuthorizationConfig
}
type IssuerFromRequest func(r *http.Request) string
func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) {
return issuerFromForwardedOrHost(path, new(issuerConfig))
}
type IssuerFromOption func(c *issuerConfig)
// WithIssuerFromCustomHeaders can be used to customize the header names used.
// The same rules apply where the first successful host is returned.
func WithIssuerFromCustomHeaders(headers ...string) IssuerFromOption {
return func(c *issuerConfig) {
for i, h := range headers {
headers[i] = http.CanonicalHeaderKey(h)
}
c.headers = headers
}
}
type issuerConfig struct {
headers []string
}
// IssuerFromForwardedOrHost tries to establish the Issuer based
// on the Forwarded header host field.
// If multiple Forwarded headers are present, the first mention
// of the host field will be used.
// If the Forwarded header is not present, no host field is found,
// or there is a parser error the Request Host will be used as a fallback.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
func IssuerFromForwardedOrHost(path string, opts ...IssuerFromOption) func(bool) (IssuerFromRequest, error) {
c := &issuerConfig{
headers: []string{http.CanonicalHeaderKey("forwarded")},
}
for _, opt := range opts {
opt(c)
}
return issuerFromForwardedOrHost(path, c)
}
func issuerFromForwardedOrHost(path string, c *issuerConfig) func(bool) (IssuerFromRequest, error) {
return func(allowInsecure bool) (IssuerFromRequest, error) {
issuerPath, err := url.Parse(path)
if err != nil {
return nil, ErrInvalidIssuerURL
}
if err := ValidateIssuerPath(issuerPath); err != nil {
return nil, err
}
return func(r *http.Request) string {
if host, ok := hostFromForwarded(r, c.headers); ok {
return dynamicIssuer(host, path, allowInsecure)
}
return dynamicIssuer(r.Host, path, allowInsecure)
}, nil
}
}
func hostFromForwarded(r *http.Request, headers []string) (host string, ok bool) {
for _, header := range headers {
hosts, err := httpforwarded.ParseParameter("host", r.Header[header])
if err != nil {
log.Printf("Err: issuer from forwarded header: %v", err) // TODO change to slog on next branch
continue
}
if len(hosts) > 0 {
return hosts[0], true
}
}
return "", false
}
func StaticIssuer(issuer string) func(bool) (IssuerFromRequest, error) {
return func(allowInsecure bool) (IssuerFromRequest, error) {
if err := ValidateIssuer(issuer, allowInsecure); err != nil {
return nil, err
}
return func(_ *http.Request) string {
return issuer
}, nil
}
}
func ValidateIssuer(issuer string, allowInsecure bool) error {
if issuer == "" {
return ErrInvalidIssuerNoIssuer
}
u, err := url.Parse(issuer)
if err != nil {
return ErrInvalidIssuerURL
}
if u.Host == "" {
return ErrInvalidIssuerMissingHost
}
if u.Scheme != "https" {
if !devLocalAllowed(u, allowInsecure) {
return ErrInvalidIssuerHTTPS
}
}
return ValidateIssuerPath(u)
}
func ValidateIssuerPath(issuer *url.URL) error {
if issuer.Fragment != "" || len(issuer.Query()) > 0 {
return ErrInvalidIssuerPath
}
return nil
}
func devLocalAllowed(url *url.URL, allowInsecure bool) bool {
if !allowInsecure {
return false
}
return url.Scheme == "http"
}
func dynamicIssuer(issuer, path string, allowInsecure bool) string {
schema := "https"
if allowInsecure {
schema = "http"
}
if len(path) > 0 && !strings.HasPrefix(path, "/") {
path = "/" + path
}
return schema + "://" + issuer + path
}
golang-github-zitadel-oidc-3.27.0/pkg/op/config_test.go 0000664 0000000 0000000 00000021016 14656014552 0022742 0 ustar 00root root 0000000 0000000 package op
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidateIssuer(t *testing.T) {
type args struct {
issuer string
allowInsecure bool
}
tests := []struct {
name string
args args
wantErr bool
}{
{
"missing issuer fails",
args{
issuer: "",
},
true,
},
{
"invalid url for issuer fails",
args{
issuer: ":issuer",
},
true,
},
{
"host for issuer missing fails",
args{
issuer: "https:///issuer",
},
true,
},
{
"host with fragment fails",
args{
issuer: "https://issuer.com/#issuer",
},
true,
},
{
"host with query fails",
args{
issuer: "https://issuer.com?issuer=me",
},
true,
},
{
"host with http fails",
args{
issuer: "http://issuer.com",
},
true,
},
{
"host with https ok",
args{
issuer: "https://issuer.com",
},
false,
},
{
"custom scheme fails",
args{
issuer: "custom://localhost:9999",
},
true,
},
{
"http with allowInsecure ok",
args{
issuer: "http://localhost:9999",
allowInsecure: true,
},
false,
},
{
"https with allowInsecure ok",
args{
issuer: "https://localhost:9999",
allowInsecure: true,
},
false,
},
{
"custom scheme with allowInsecure fails",
args{
issuer: "custom://localhost:9999",
allowInsecure: true,
},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateIssuer(tt.args.issuer, tt.args.allowInsecure); (err != nil) != tt.wantErr {
t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestValidateIssuerPath(t *testing.T) {
type args struct {
issuerPath *url.URL
}
tests := []struct {
name string
args args
wantErr bool
}{
{
"empty ok",
args{func() *url.URL {
u, _ := url.Parse("")
return u
}()},
false,
},
{
"custom ok",
args{func() *url.URL {
u, _ := url.Parse("/custom")
return u
}()},
false,
},
{
"fragment fails",
args{func() *url.URL {
u, _ := url.Parse("#fragment")
return u
}()},
true,
},
{
"query fails",
args{func() *url.URL {
u, _ := url.Parse("?query=value")
return u
}()},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateIssuerPath(tt.args.issuerPath); (err != nil) != tt.wantErr {
t.Errorf("ValidateIssuerPath() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestIssuerFromHost(t *testing.T) {
type args struct {
path string
allowInsecure bool
target string
}
type res struct {
issuer string
err error
}
tests := []struct {
name string
args args
res res
}{
{
"invalid issuer path",
args{
path: "/#fragment",
allowInsecure: false,
},
res{
issuer: "",
err: ErrInvalidIssuerPath,
},
},
{
"empty path secure",
args{
path: "",
allowInsecure: false,
target: "https://issuer.com",
},
res{
issuer: "https://issuer.com",
err: nil,
},
},
{
"custom path secure",
args{
path: "/custom/",
allowInsecure: false,
target: "https://issuer.com",
},
res{
issuer: "https://issuer.com/custom/",
err: nil,
},
},
{
"custom path no leading slash",
args{
path: "custom/",
allowInsecure: false,
target: "https://issuer.com",
},
res{
issuer: "https://issuer.com/custom/",
err: nil,
},
},
{
"empty path unsecure",
args{
path: "",
allowInsecure: true,
target: "http://issuer.com",
},
res{
issuer: "http://issuer.com",
err: nil,
},
},
{
"custom path insecure",
args{
path: "/custom/",
allowInsecure: true,
target: "http://issuer.com",
},
res{
issuer: "http://issuer.com/custom/",
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
issuer, err := IssuerFromHost(tt.args.path)(tt.args.allowInsecure)
if tt.res.err == nil {
assert.NoError(t, err)
req := httptest.NewRequest("", tt.args.target, nil)
assert.Equal(t, tt.res.issuer, issuer(req))
}
if tt.res.err != nil {
assert.ErrorIs(t, err, tt.res.err)
}
})
}
}
func TestIssuerFromForwardedOrHost(t *testing.T) {
type args struct {
path string
opts []IssuerFromOption
target string
header map[string][]string
}
type res struct {
issuer string
}
tests := []struct {
name string
args args
res res
}{
{
"header parse error",
args{
path: "/custom/",
target: "https://issuer.com",
header: map[string][]string{"Forwarded": {"~~~~"}},
},
res{
issuer: "https://issuer.com/custom/",
},
},
{
"no forwarded header",
args{
path: "/custom/",
target: "https://issuer.com",
},
res{
issuer: "https://issuer.com/custom/",
},
},
// by=;for=;host=;proto=
{
"forwarded header without host",
args{
path: "/custom/",
target: "https://issuer.com",
header: map[string][]string{"Forwarded": {
`by=identifier;for=identifier;proto=https`,
}},
},
res{
issuer: "https://issuer.com/custom/",
},
},
{
"forwarded header with host",
args{
path: "/custom/",
target: "https://issuer.com",
header: map[string][]string{"Forwarded": {
`by=identifier;for=identifier;host=first.com;proto=https`,
}},
},
res{
issuer: "https://first.com/custom/",
},
},
{
"forwarded header with multiple hosts",
args{
path: "/custom/",
target: "https://issuer.com",
header: map[string][]string{"Forwarded": {
`by=identifier;for=identifier;host=first.com;proto=https,host=second.com`,
}},
},
res{
issuer: "https://first.com/custom/",
},
},
{
"multiple forwarded headers hosts",
args{
path: "/custom/",
target: "https://issuer.com",
header: map[string][]string{"Forwarded": {
`by=identifier;for=identifier;host=first.com;proto=https,host=second.com`,
`by=identifier;for=identifier;host=third.com;proto=https`,
}},
},
res{
issuer: "https://first.com/custom/",
},
},
{
"custom header first",
args{
path: "/custom/",
target: "https://issuer.com",
header: map[string][]string{
"Forwarded": {
`by=identifier;for=identifier;host=first.com;proto=https,host=second.com`,
`by=identifier;for=identifier;host=third.com;proto=https`,
},
"X-Custom-Forwarded": {
`by=identifier;for=identifier;host=custom.com;proto=https,host=custom2.com`,
},
},
opts: []IssuerFromOption{
WithIssuerFromCustomHeaders("x-custom-forwarded"),
},
},
res{
issuer: "https://custom.com/custom/",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
issuer, err := IssuerFromForwardedOrHost(tt.args.path, tt.args.opts...)(false)
require.NoError(t, err)
req := httptest.NewRequest("", tt.args.target, nil)
for k, v := range tt.args.header {
req.Header[http.CanonicalHeaderKey(k)] = v
}
assert.Equal(t, tt.res.issuer, issuer(req))
})
}
}
func TestStaticIssuer(t *testing.T) {
type args struct {
issuer string
allowInsecure bool
}
type res struct {
issuer string
err error
}
tests := []struct {
name string
args args
res res
}{
{
"invalid issuer",
args{
issuer: "",
allowInsecure: false,
},
res{
issuer: "",
err: ErrInvalidIssuerNoIssuer,
},
},
{
"empty path secure",
args{
issuer: "https://issuer.com",
allowInsecure: false,
},
res{
issuer: "https://issuer.com",
err: nil,
},
},
{
"custom path secure",
args{
issuer: "https://issuer.com/custom/",
allowInsecure: false,
},
res{
issuer: "https://issuer.com/custom/",
err: nil,
},
},
{
"unsecure",
args{
issuer: "http://issuer.com",
allowInsecure: true,
},
res{
issuer: "http://issuer.com",
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
issuer, err := StaticIssuer(tt.args.issuer)(tt.args.allowInsecure)
if tt.res.err == nil {
assert.NoError(t, err)
assert.Equal(t, tt.res.issuer, issuer(nil))
}
if tt.res.err != nil {
assert.ErrorIs(t, err, tt.res.err)
}
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/op/context.go 0000664 0000000 0000000 00000002677 14656014552 0022136 0 ustar 00root root 0000000 0000000 package op
import (
"context"
"net/http"
)
type key int
const (
issuerKey key = 0
)
type IssuerInterceptor struct {
issuerFromRequest IssuerFromRequest
}
// NewIssuerInterceptor will set the issuer into the context
// by the provided IssuerFromRequest (e.g. returned from StaticIssuer or IssuerFromHost)
func NewIssuerInterceptor(issuerFromRequest IssuerFromRequest) *IssuerInterceptor {
return &IssuerInterceptor{
issuerFromRequest: issuerFromRequest,
}
}
func (i *IssuerInterceptor) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
i.setIssuerCtx(w, r, next)
})
}
func (i *IssuerInterceptor) HandlerFunc(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
i.setIssuerCtx(w, r, next)
}
}
// IssuerFromContext reads the issuer from the context (set by an IssuerInterceptor)
// it will return an empty string if not found
func IssuerFromContext(ctx context.Context) string {
ctxIssuer, _ := ctx.Value(issuerKey).(string)
return ctxIssuer
}
// ContextWithIssuer returns a new context with issuer set to it.
func ContextWithIssuer(ctx context.Context, issuer string) context.Context {
return context.WithValue(ctx, issuerKey, issuer)
}
func (i *IssuerInterceptor) setIssuerCtx(w http.ResponseWriter, r *http.Request, next http.Handler) {
r = r.WithContext(ContextWithIssuer(r.Context(), i.issuerFromRequest(r)))
next.ServeHTTP(w, r)
}
golang-github-zitadel-oidc-3.27.0/pkg/op/context_test.go 0000664 0000000 0000000 00000002355 14656014552 0023166 0 ustar 00root root 0000000 0000000 package op
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIssuerInterceptor(t *testing.T) {
type fields struct {
issuerFromRequest IssuerFromRequest
}
type args struct {
r *http.Request
next http.Handler
}
type res struct {
issuer string
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"empty",
fields{
func(r *http.Request) string {
return ""
},
},
args{},
res{
issuer: "",
},
},
{
"static",
fields{
func(r *http.Request) string {
return "static"
},
},
args{},
res{
issuer: "static",
},
},
{
"host",
fields{
func(r *http.Request) string {
return r.Host
},
},
args{},
res{
issuer: "issuer.com",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
i := NewIssuerInterceptor(tt.fields.issuerFromRequest)
next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
assert.Equal(t, tt.res.issuer, IssuerFromContext(r.Context()))
})
req := httptest.NewRequest("", "https://issuer.com", nil)
i.Handler(next).ServeHTTP(nil, req)
i.HandlerFunc(next).ServeHTTP(nil, req)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/op/crypto.go 0000664 0000000 0000000 00000000730 14656014552 0021756 0 ustar 00root root 0000000 0000000 package op
import (
"github.com/zitadel/oidc/v3/pkg/crypto"
)
type Crypto interface {
Encrypt(string) (string, error)
Decrypt(string) (string, error)
}
type aesCrypto struct {
key string
}
func NewAESCrypto(key [32]byte) Crypto {
return &aesCrypto{key: string(key[:32])}
}
func (c *aesCrypto) Encrypt(s string) (string, error) {
return crypto.EncryptAES(s, c.key)
}
func (c *aesCrypto) Decrypt(s string) (string, error) {
return crypto.DecryptAES(s, c.key)
}
golang-github-zitadel-oidc-3.27.0/pkg/op/device.go 0000664 0000000 0000000 00000024105 14656014552 0021677 0 ustar 00root root 0000000 0000000 package op
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"math/big"
"net/http"
"net/url"
"strings"
"time"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
strs "github.com/zitadel/oidc/v3/pkg/strings"
)
type DeviceAuthorizationConfig struct {
Lifetime time.Duration
PollInterval time.Duration
// UserFormURL is the complete URL where the user must go to authorize the device.
// Deprecated: use UserFormPath instead.
UserFormURL string
// UserFormPath is the path where the user must go to authorize the device.
// The hostname for the URL is taken from the request by IssuerFromContext.
UserFormPath string
UserCode UserCodeConfig
}
type UserCodeConfig struct {
CharSet string
CharAmount int
DashInterval int
}
const (
CharSetBase20 = "BCDFGHJKLMNPQRSTVWXZ"
CharSetDigits = "0123456789"
)
var (
UserCodeBase20 = UserCodeConfig{
CharSet: CharSetBase20,
CharAmount: 8,
DashInterval: 4,
}
UserCodeDigits = UserCodeConfig{
CharSet: CharSetDigits,
CharAmount: 9,
DashInterval: 3,
}
)
func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if err := DeviceAuthorization(w, r, o); err != nil {
RequestError(w, r, err, o.Logger())
}
}
}
func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error {
ctx, span := tracer.Start(r.Context(), "DeviceAuthorization")
r = r.WithContext(ctx)
defer span.End()
req, err := ParseDeviceCodeRequest(r, o)
if err != nil {
return err
}
response, err := createDeviceAuthorization(r.Context(), req, req.ClientID, o)
if err != nil {
return err
}
httphelper.MarshalJSON(w, response)
return nil
}
func createDeviceAuthorization(ctx context.Context, req *oidc.DeviceAuthorizationRequest, clientID string, o OpenIDProvider) (*oidc.DeviceAuthorizationResponse, error) {
ctx, span := tracer.Start(ctx, "createDeviceAuthorization")
defer span.End()
storage, err := assertDeviceStorage(o.Storage())
if err != nil {
return nil, err
}
config := o.DeviceAuthorization()
deviceCode, err := NewDeviceCode(RecommendedDeviceCodeBytes)
if err != nil {
return nil, NewStatusError(err, http.StatusInternalServerError)
}
userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval)
if err != nil {
return nil, NewStatusError(err, http.StatusInternalServerError)
}
expires := time.Now().Add(config.Lifetime)
err = storage.StoreDeviceAuthorization(ctx, clientID, deviceCode, userCode, expires, req.Scopes)
if err != nil {
return nil, NewStatusError(err, http.StatusInternalServerError)
}
var verification *url.URL
if config.UserFormURL != "" {
if verification, err = url.Parse(config.UserFormURL); err != nil {
err = oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for device user form")
return nil, NewStatusError(err, http.StatusInternalServerError)
}
} else {
if verification, err = url.Parse(IssuerFromContext(ctx)); err != nil {
err = oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for issuer")
return nil, NewStatusError(err, http.StatusInternalServerError)
}
verification.Path = config.UserFormPath
}
response := &oidc.DeviceAuthorizationResponse{
DeviceCode: deviceCode,
UserCode: userCode,
VerificationURI: verification.String(),
ExpiresIn: int(config.Lifetime / time.Second),
Interval: int(config.PollInterval / time.Second),
}
verification.RawQuery = "user_code=" + userCode
response.VerificationURIComplete = verification.String()
return response, nil
}
func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) {
ctx, span := tracer.Start(r.Context(), "ParseDeviceCodeRequest")
r = r.WithContext(ctx)
defer span.End()
clientID, _, err := ClientIDFromRequest(r, o)
if err != nil {
return nil, err
}
client, err := o.Storage().GetClientByClientID(r.Context(), clientID)
if err != nil {
return nil, err
}
if !ValidateGrantType(client, oidc.GrantTypeDeviceCode) {
return nil, oidc.ErrUnauthorizedClient().WithDescription("client missing grant type " + string(oidc.GrantTypeCode))
}
req := new(oidc.DeviceAuthorizationRequest)
if err := o.Decoder().Decode(req, r.Form); err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse device authentication request").WithParent(err)
}
req.ClientID = clientID
return req, nil
}
// 16 bytes gives 128 bit of entropy.
// results in a 22 character base64 encoded string.
const RecommendedDeviceCodeBytes = 16
func NewDeviceCode(nBytes int) (string, error) {
bytes := make([]byte, nBytes)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("%w getting entropy for device code", err)
}
return base64.RawURLEncoding.EncodeToString(bytes), nil
}
func NewUserCode(charSet []rune, charAmount, dashInterval int) (string, error) {
var buf strings.Builder
if dashInterval > 0 {
buf.Grow(charAmount + charAmount/dashInterval - 1)
} else {
buf.Grow(charAmount)
}
max := big.NewInt(int64(len(charSet)))
for i := 0; i < charAmount; i++ {
if dashInterval != 0 && i != 0 && i%dashInterval == 0 {
buf.WriteByte('-')
}
bi, err := rand.Int(rand.Reader, max)
if err != nil {
return "", fmt.Errorf("%w getting entropy for user code", err)
}
buf.WriteRune(charSet[int(bi.Int64())])
}
return buf.String(), nil
}
func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
ctx, span := tracer.Start(r.Context(), "DeviceAccessToken")
defer span.End()
r = r.WithContext(ctx)
if err := deviceAccessToken(w, r, exchanger); err != nil {
RequestError(w, r, err, exchanger.Logger())
}
}
func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) error {
// use a limited context timeout shorter as the default
// poll interval of 5 seconds.
ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
defer cancel()
r = r.WithContext(ctx)
clientID, clientAuthenticated, err := ClientIDFromRequest(r, exchanger)
if err != nil {
return err
}
req, err := ParseDeviceAccessTokenRequest(r, exchanger)
if err != nil {
return err
}
tokenRequest, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger)
if err != nil {
return err
}
client, err := exchanger.Storage().GetClientByClientID(ctx, clientID)
if err != nil {
return err
}
if clientAuthenticated != IsConfidentialType(client) {
return oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials).
WithDescription("confidential client requires authentication")
}
resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client)
if err != nil {
return err
}
httphelper.MarshalJSON(w, resp)
return nil
}
func ParseDeviceAccessTokenRequest(r *http.Request, exchanger Exchanger) (*oidc.DeviceAccessTokenRequest, error) {
req := new(oidc.DeviceAccessTokenRequest)
if err := exchanger.Decoder().Decode(req, r.PostForm); err != nil {
return nil, err
}
return req, nil
}
// DeviceAuthorizationState describes the current state of
// the device authorization flow.
// It implements the [IDTokenRequest] interface.
type DeviceAuthorizationState struct {
ClientID string
Audience []string
Scopes []string
Expires time.Time // The time after we consider the authorization request timed-out
Done bool // The user authenticated and approved the authorization request
Denied bool // The user authenticated and denied the authorization request
// The following fields are populated after Done == true
Subject string
AMR []string
AuthTime time.Time
}
func (r *DeviceAuthorizationState) GetAMR() []string {
return r.AMR
}
func (r *DeviceAuthorizationState) GetAudience() []string {
if !strs.Contains(r.Audience, r.ClientID) {
r.Audience = append(r.Audience, r.ClientID)
}
return r.Audience
}
func (r *DeviceAuthorizationState) GetAuthTime() time.Time {
return r.AuthTime
}
func (r *DeviceAuthorizationState) GetClientID() string {
return r.ClientID
}
func (r *DeviceAuthorizationState) GetScopes() []string {
return r.Scopes
}
func (r *DeviceAuthorizationState) GetSubject() string {
return r.Subject
}
func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) {
ctx, span := tracer.Start(ctx, "CheckDeviceAuthorizationState")
defer span.End()
storage, err := assertDeviceStorage(exchanger.Storage())
if err != nil {
return nil, err
}
state, err := storage.GetDeviceAuthorizatonState(ctx, clientID, deviceCode)
if errors.Is(err, context.DeadlineExceeded) {
return nil, oidc.ErrSlowDown().WithParent(err)
}
if err != nil {
return nil, oidc.ErrAccessDenied().WithParent(err)
}
if state.Denied {
return state, oidc.ErrAccessDenied()
}
if state.Done {
return state, nil
}
if time.Now().After(state.Expires) {
return state, oidc.ErrExpiredDeviceCode()
}
return state, oidc.ErrAuthorizationPending()
}
func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) {
/* TODO(v4):
Change the TokenRequest argument type to *DeviceAuthorizationState.
Breaking change that can not be done for v3.
*/
ctx, span := tracer.Start(ctx, "CreateDeviceTokenResponse")
defer span.End()
accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, client.AccessTokenType(), creator, client, "")
if err != nil {
return nil, err
}
response := &oidc.AccessTokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: oidc.BearerToken,
ExpiresIn: uint64(validity.Seconds()),
}
// TODO(v4): remove type assertion
if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && strs.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) {
response.IDToken, err = CreateIDToken(ctx, IssuerFromContext(ctx), idTokenRequest, client.IDTokenLifetime(), accessToken, "", creator.Storage(), client)
if err != nil {
return nil, err
}
}
return response, nil
}
golang-github-zitadel-oidc-3.27.0/pkg/op/device_test.go 0000664 0000000 0000000 00000031553 14656014552 0022743 0 ustar 00root root 0000000 0000000 package op_test
import (
"context"
"crypto/rand"
"encoding/base64"
"io"
mr "math/rand"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/example/server/storage"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
)
func Test_deviceAuthorizationHandler(t *testing.T) {
type conf struct {
UserFormURL string
UserFormPath string
}
tests := []struct {
name string
conf conf
}{
{
name: "UserFormURL",
conf: conf{
UserFormURL: "https://localhost:9998/device",
},
},
{
name: "UserFormPath",
conf: conf{
UserFormPath: "/device",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf := gu.PtrCopy(testConfig)
conf.DeviceAuthorization.UserFormURL = tt.conf.UserFormURL
conf.DeviceAuthorization.UserFormPath = tt.conf.UserFormPath
provider := newTestProvider(conf)
req := &oidc.DeviceAuthorizationRequest{
Scopes: []string{"foo", "bar"},
ClientID: "device",
}
values := make(url.Values)
testProvider.Encoder().Encode(req, values)
body := strings.NewReader(values.Encode())
r := httptest.NewRequest(http.MethodPost, "/", body)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
r = r.WithContext(op.ContextWithIssuer(r.Context(), testIssuer))
w := httptest.NewRecorder()
runWithRandReader(mr.New(mr.NewSource(1)), func() {
op.DeviceAuthorizationHandler(provider)(w, r)
})
result := w.Result()
assert.Less(t, result.StatusCode, 300)
got, _ := io.ReadAll(result.Body)
assert.JSONEq(t, `{"device_code":"Uv38ByGCZU8WP18PmmIdcg", "expires_in":300, "interval":5, "user_code":"JKRV-FRGK", "verification_uri":"https://localhost:9998/device", "verification_uri_complete":"https://localhost:9998/device?user_code=JKRV-FRGK"}`, string(got))
})
}
}
func TestParseDeviceCodeRequest(t *testing.T) {
tests := []struct {
name string
req *oidc.DeviceAuthorizationRequest
wantErr bool
}{
{
name: "empty request",
wantErr: true,
},
{
name: "missing grant type",
req: &oidc.DeviceAuthorizationRequest{
Scopes: oidc.SpaceDelimitedArray{"foo", "bar"},
ClientID: "web",
},
wantErr: true,
},
{
name: "client not found",
req: &oidc.DeviceAuthorizationRequest{
Scopes: oidc.SpaceDelimitedArray{"foo", "bar"},
ClientID: "foobar",
},
wantErr: true,
},
{
name: "success",
req: &oidc.DeviceAuthorizationRequest{
Scopes: oidc.SpaceDelimitedArray{"foo", "bar"},
ClientID: "device",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var body io.Reader
if tt.req != nil {
values := make(url.Values)
testProvider.Encoder().Encode(tt.req, values)
body = strings.NewReader(values.Encode())
}
r := httptest.NewRequest(http.MethodPost, "/", body)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
got, err := op.ParseDeviceCodeRequest(r, testProvider)
if tt.wantErr {
require.Error(t, err)
return
}
assert.Equal(t, tt.req, got)
})
}
}
func runWithRandReader(r io.Reader, f func()) {
originalReader := rand.Reader
rand.Reader = r
defer func() {
rand.Reader = originalReader
}()
f()
}
func TestNewDeviceCode(t *testing.T) {
t.Run("reader error", func(t *testing.T) {
runWithRandReader(errReader{}, func() {
_, err := op.NewDeviceCode(16)
require.Error(t, err)
})
})
t.Run("different lengths, rand reader", func(t *testing.T) {
for i := 1; i <= 32; i++ {
got, err := op.NewDeviceCode(i)
require.NoError(t, err)
assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i))
}
})
}
func TestNewUserCode(t *testing.T) {
type args struct {
charset []rune
charAmount int
dashInterval int
}
tests := []struct {
name string
args args
reader io.Reader
want string
wantErr bool
}{
{
name: "reader error",
args: args{
charset: []rune(op.CharSetBase20),
charAmount: 8,
dashInterval: 4,
},
reader: errReader{},
wantErr: true,
},
{
name: "base20",
args: args{
charset: []rune(op.CharSetBase20),
charAmount: 8,
dashInterval: 4,
},
reader: mr.New(mr.NewSource(1)),
want: "XKCD-HTTD",
},
{
name: "digits",
args: args{
charset: []rune(op.CharSetDigits),
charAmount: 9,
dashInterval: 3,
},
reader: mr.New(mr.NewSource(1)),
want: "271-256-225",
},
{
name: "no dashes",
args: args{
charset: []rune(op.CharSetDigits),
charAmount: 9,
},
reader: mr.New(mr.NewSource(1)),
want: "271256225",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runWithRandReader(tt.reader, func() {
got, err := op.NewUserCode(tt.args.charset, tt.args.charAmount, tt.args.dashInterval)
if tt.wantErr {
require.ErrorIs(t, err, io.ErrNoProgress)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want, got)
})
})
}
t.Run("crypto/rand", func(t *testing.T) {
const testN = 100000
for _, c := range []op.UserCodeConfig{op.UserCodeBase20, op.UserCodeDigits} {
t.Run(c.CharSet, func(t *testing.T) {
results := make(map[string]int)
for i := 0; i < testN; i++ {
code, err := op.NewUserCode([]rune(c.CharSet), c.CharAmount, c.DashInterval)
require.NoError(t, err)
results[code]++
}
t.Log(results)
var duplicates int
for code, count := range results {
assert.Less(t, count, 3, code)
if count == 2 {
duplicates++
}
}
})
}
})
}
func BenchmarkNewUserCode(b *testing.B) {
type args struct {
charset []rune
charAmount int
dashInterval int
}
tests := []struct {
name string
args args
reader io.Reader
}{
{
name: "math rand, base20",
args: args{
charset: []rune(op.CharSetBase20),
charAmount: 8,
dashInterval: 4,
},
reader: mr.New(mr.NewSource(1)),
},
{
name: "math rand, digits",
args: args{
charset: []rune(op.CharSetDigits),
charAmount: 9,
dashInterval: 3,
},
reader: mr.New(mr.NewSource(1)),
},
{
name: "crypto rand, base20",
args: args{
charset: []rune(op.CharSetBase20),
charAmount: 8,
dashInterval: 4,
},
reader: rand.Reader,
},
{
name: "crypto rand, digits",
args: args{
charset: []rune(op.CharSetDigits),
charAmount: 9,
dashInterval: 3,
},
reader: rand.Reader,
},
}
for _, tt := range tests {
runWithRandReader(tt.reader, func() {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := op.NewUserCode(tt.args.charset, tt.args.charAmount, tt.args.dashInterval)
require.NoError(b, err)
}
})
})
}
}
func TestDeviceAccessToken(t *testing.T) {
storage := testProvider.Storage().(*storage.Storage)
storage.StoreDeviceAuthorization(context.Background(), "native", "qwerty", "yuiop", time.Now().Add(time.Minute), []string{"foo"})
storage.CompleteDeviceAuthorization(context.Background(), "yuiop", "tim")
values := make(url.Values)
values.Set("client_id", "native")
values.Set("grant_type", string(oidc.GrantTypeDeviceCode))
values.Set("device_code", "qwerty")
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(values.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
op.DeviceAccessToken(w, r, testProvider)
result := w.Result()
got, _ := io.ReadAll(result.Body)
t.Log(string(got))
assert.Less(t, result.StatusCode, 300)
assert.NotEmpty(t, string(got))
}
func TestCheckDeviceAuthorizationState(t *testing.T) {
now := time.Now()
storage := testProvider.Storage().(*storage.Storage)
storage.StoreDeviceAuthorization(context.Background(), "native", "pending", "pending", now.Add(time.Minute), []string{"foo"})
storage.StoreDeviceAuthorization(context.Background(), "native", "denied", "denied", now.Add(time.Minute), []string{"foo"})
storage.StoreDeviceAuthorization(context.Background(), "native", "completed", "completed", now.Add(time.Minute), []string{"foo"})
storage.StoreDeviceAuthorization(context.Background(), "native", "expired", "expired", now.Add(-time.Minute), []string{"foo"})
storage.DenyDeviceAuthorization(context.Background(), "denied")
storage.CompleteDeviceAuthorization(context.Background(), "completed", "tim")
exceededCtx, cancel := context.WithTimeout(context.Background(), -time.Second)
defer cancel()
type args struct {
ctx context.Context
clientID string
deviceCode string
}
tests := []struct {
name string
args args
want *op.DeviceAuthorizationState
wantErr error
}{
{
name: "pending",
args: args{
ctx: context.Background(),
clientID: "native",
deviceCode: "pending",
},
want: &op.DeviceAuthorizationState{
ClientID: "native",
Scopes: []string{"foo"},
Expires: now.Add(time.Minute),
},
wantErr: oidc.ErrAuthorizationPending(),
},
{
name: "slow down",
args: args{
ctx: exceededCtx,
clientID: "native",
deviceCode: "ok",
},
wantErr: oidc.ErrSlowDown(),
},
{
name: "wrong client",
args: args{
ctx: context.Background(),
clientID: "foo",
deviceCode: "ok",
},
wantErr: oidc.ErrAccessDenied(),
},
{
name: "denied",
args: args{
ctx: context.Background(),
clientID: "native",
deviceCode: "denied",
},
want: &op.DeviceAuthorizationState{
ClientID: "native",
Scopes: []string{"foo"},
Expires: now.Add(time.Minute),
Denied: true,
},
wantErr: oidc.ErrAccessDenied(),
},
{
name: "completed",
args: args{
ctx: context.Background(),
clientID: "native",
deviceCode: "completed",
},
want: &op.DeviceAuthorizationState{
ClientID: "native",
Scopes: []string{"foo"},
Expires: now.Add(time.Minute),
Subject: "tim",
Done: true,
},
},
{
name: "expired",
args: args{
ctx: context.Background(),
clientID: "native",
deviceCode: "expired",
},
want: &op.DeviceAuthorizationState{
ClientID: "native",
Scopes: []string{"foo"},
Expires: now.Add(-time.Minute),
},
wantErr: oidc.ErrExpiredDeviceCode(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := op.CheckDeviceAuthorizationState(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, testProvider)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func TestCreateDeviceTokenResponse(t *testing.T) {
tests := []struct {
name string
tokenRequest op.TokenRequest
wantAccessToken bool
wantRefreshToken bool
wantIDToken bool
wantErr bool
}{
{
name: "access token",
tokenRequest: &op.DeviceAuthorizationState{
ClientID: "client1",
Subject: "id1",
AMR: []string{"password"},
AuthTime: time.Now(),
},
wantAccessToken: true,
},
{
name: "access and refresh tokens",
tokenRequest: &op.DeviceAuthorizationState{
ClientID: "client1",
Subject: "id1",
AMR: []string{"password"},
AuthTime: time.Now(),
Scopes: []string{oidc.ScopeOfflineAccess},
},
wantAccessToken: true,
wantRefreshToken: true,
},
{
name: "access and id token",
tokenRequest: &op.DeviceAuthorizationState{
ClientID: "client1",
Subject: "id1",
AMR: []string{"password"},
AuthTime: time.Now(),
Scopes: []string{oidc.ScopeOpenID},
},
wantAccessToken: true,
wantIDToken: true,
},
{
name: "access, refresh and id token",
tokenRequest: &op.DeviceAuthorizationState{
ClientID: "client1",
Subject: "id1",
AMR: []string{"password"},
AuthTime: time.Now(),
Scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID},
},
wantAccessToken: true,
wantRefreshToken: true,
wantIDToken: true,
},
{
name: "id token creation error",
tokenRequest: &op.DeviceAuthorizationState{
ClientID: "client1",
Subject: "foobar",
AMR: []string{"password"},
AuthTime: time.Now(),
Scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := testProvider.Storage().GetClientByClientID(context.Background(), "native")
require.NoError(t, err)
got, err := op.CreateDeviceTokenResponse(context.Background(), tt.tokenRequest, testProvider, client)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.InDelta(t, 300, got.ExpiresIn, 2)
if tt.wantAccessToken {
assert.NotEmpty(t, got.AccessToken, "access token")
}
if tt.wantRefreshToken {
assert.NotEmpty(t, got.RefreshToken, "refresh token")
}
if tt.wantIDToken {
assert.NotEmpty(t, got.IDToken, "id token")
}
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/op/discovery.go 0000664 0000000 0000000 00000021417 14656014552 0022452 0 ustar 00root root 0000000 0000000 package op
import (
"context"
"net/http"
jose "github.com/go-jose/go-jose/v4"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
type DiscoverStorage interface {
SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error)
}
var DefaultSupportedScopes = []string{
oidc.ScopeOpenID,
oidc.ScopeProfile,
oidc.ScopeEmail,
oidc.ScopePhone,
oidc.ScopeAddress,
oidc.ScopeOfflineAccess,
}
func discoveryHandler(c Configuration, s DiscoverStorage) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
Discover(w, CreateDiscoveryConfig(r.Context(), c, s))
}
}
func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) {
httphelper.MarshalJSON(w, config)
}
func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration {
issuer := IssuerFromContext(ctx)
return &oidc.DiscoveryConfiguration{
Issuer: issuer,
AuthorizationEndpoint: config.AuthorizationEndpoint().Absolute(issuer),
TokenEndpoint: config.TokenEndpoint().Absolute(issuer),
IntrospectionEndpoint: config.IntrospectionEndpoint().Absolute(issuer),
UserinfoEndpoint: config.UserinfoEndpoint().Absolute(issuer),
RevocationEndpoint: config.RevocationEndpoint().Absolute(issuer),
EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer),
JwksURI: config.KeysEndpoint().Absolute(issuer),
DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer),
ScopesSupported: Scopes(config),
ResponseTypesSupported: ResponseTypes(config),
GrantTypesSupported: GrantTypes(config),
SubjectTypesSupported: SubjectTypes(config),
IDTokenSigningAlgValuesSupported: SigAlgorithms(ctx, storage),
RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(config),
TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(config),
TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(config),
IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(config),
IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(config),
RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(config),
RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(config),
ClaimsSupported: SupportedClaims(config),
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
UILocalesSupported: config.SupportedUILocales(),
RequestParameterSupported: config.RequestObjectSupported(),
}
}
func createDiscoveryConfigV2(ctx context.Context, config Configuration, storage DiscoverStorage, endpoints *Endpoints) *oidc.DiscoveryConfiguration {
issuer := IssuerFromContext(ctx)
return &oidc.DiscoveryConfiguration{
Issuer: issuer,
AuthorizationEndpoint: endpoints.Authorization.Absolute(issuer),
TokenEndpoint: endpoints.Token.Absolute(issuer),
IntrospectionEndpoint: endpoints.Introspection.Absolute(issuer),
UserinfoEndpoint: endpoints.Userinfo.Absolute(issuer),
RevocationEndpoint: endpoints.Revocation.Absolute(issuer),
EndSessionEndpoint: endpoints.EndSession.Absolute(issuer),
JwksURI: endpoints.JwksURI.Absolute(issuer),
DeviceAuthorizationEndpoint: endpoints.DeviceAuthorization.Absolute(issuer),
ScopesSupported: Scopes(config),
ResponseTypesSupported: ResponseTypes(config),
GrantTypesSupported: GrantTypes(config),
SubjectTypesSupported: SubjectTypes(config),
IDTokenSigningAlgValuesSupported: SigAlgorithms(ctx, storage),
RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(config),
TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(config),
TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(config),
IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(config),
IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(config),
RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(config),
RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(config),
ClaimsSupported: SupportedClaims(config),
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
UILocalesSupported: config.SupportedUILocales(),
RequestParameterSupported: config.RequestObjectSupported(),
}
}
func Scopes(c Configuration) []string {
return DefaultSupportedScopes // TODO: config
}
func ResponseTypes(c Configuration) []string {
return []string{
string(oidc.ResponseTypeCode),
string(oidc.ResponseTypeIDTokenOnly),
string(oidc.ResponseTypeIDToken),
} // TODO: ok for now, check later if dynamic needed
}
func GrantTypes(c Configuration) []oidc.GrantType {
grantTypes := []oidc.GrantType{
oidc.GrantTypeCode,
oidc.GrantTypeImplicit,
}
if c.GrantTypeRefreshTokenSupported() {
grantTypes = append(grantTypes, oidc.GrantTypeRefreshToken)
}
if c.GrantTypeClientCredentialsSupported() {
grantTypes = append(grantTypes, oidc.GrantTypeClientCredentials)
}
if c.GrantTypeTokenExchangeSupported() {
grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange)
}
if c.GrantTypeJWTAuthorizationSupported() {
grantTypes = append(grantTypes, oidc.GrantTypeBearer)
}
if c.GrantTypeDeviceCodeSupported() {
grantTypes = append(grantTypes, oidc.GrantTypeDeviceCode)
}
return grantTypes
}
func SubjectTypes(c Configuration) []string {
return []string{"public"} //TODO: config
}
func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string {
ctx, span := tracer.Start(ctx, "SigAlgorithms")
defer span.End()
algorithms, err := storage.SignatureAlgorithms(ctx)
if err != nil {
return nil
}
algs := make([]string, len(algorithms))
for i, algorithm := range algorithms {
algs[i] = string(algorithm)
}
return algs
}
func RequestObjectSigAlgorithms(c Configuration) []string {
if !c.RequestObjectSupported() {
return nil
}
return c.RequestObjectSigningAlgorithmsSupported()
}
func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodNone,
oidc.AuthMethodBasic,
}
if c.AuthMethodPostSupported() {
authMethods = append(authMethods, oidc.AuthMethodPost)
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func TokenSigAlgorithms(c Configuration) []string {
if !c.AuthMethodPrivateKeyJWTSupported() {
return nil
}
return c.TokenEndpointSigningAlgorithmsSupported()
}
func IntrospectionSigAlgorithms(c Configuration) []string {
if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() {
return nil
}
return c.IntrospectionEndpointSigningAlgorithmsSupported()
}
func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodBasic,
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func RevocationSigAlgorithms(c Configuration) []string {
if !c.RevocationAuthMethodPrivateKeyJWTSupported() {
return nil
}
return c.RevocationEndpointSigningAlgorithmsSupported()
}
func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodNone,
oidc.AuthMethodBasic,
}
if c.AuthMethodPostSupported() {
authMethods = append(authMethods, oidc.AuthMethodPost)
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func SupportedClaims(c Configuration) []string {
provider, ok := c.(*Provider)
if ok && provider.config.SupportedClaims != nil {
return provider.config.SupportedClaims
}
return DefaultSupportedClaims
}
func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
if c.CodeMethodS256Supported() {
codeMethods = append(codeMethods, oidc.CodeChallengeMethodS256)
}
return codeMethods
}
golang-github-zitadel-oidc-3.27.0/pkg/op/discovery_test.go 0000664 0000000 0000000 00000033573 14656014552 0023517 0 ustar 00root root 0000000 0000000 package op_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
jose "github.com/go-jose/go-jose/v4"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/oidc/v3/pkg/op/mock"
)
func TestDiscover(t *testing.T) {
type args struct {
w http.ResponseWriter
config *oidc.DiscoveryConfiguration
}
tests := []struct {
name string
args args
}{
{
"OK",
args{
httptest.NewRecorder(),
&oidc.DiscoveryConfiguration{Issuer: "https://issuer.com"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
op.Discover(tt.args.w, tt.args.config)
rec := tt.args.w.(*httptest.ResponseRecorder)
require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t,
`{"issuer":"https://issuer.com","request_uri_parameter_supported":false}
`,
rec.Body.String())
})
}
}
func TestCreateDiscoveryConfig(t *testing.T) {
type args struct {
ctx context.Context
c op.Configuration
s op.DiscoverStorage
}
tests := []struct {
name string
args args
want *oidc.DiscoveryConfiguration
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.CreateDiscoveryConfig(tt.args.ctx, tt.args.c, tt.args.s)
assert.Equal(t, tt.want, got)
})
}
}
func Test_scopes(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"default Scopes",
args{},
op.DefaultSupportedScopes,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.Scopes(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_ResponseTypes(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"code and implicit flow",
args{},
[]string{"code", "id_token", "id_token token"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.ResponseTypes(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_GrantTypes(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []oidc.GrantType
}{
{
"code and implicit flow",
args{
func() op.Configuration {
c := mock.NewMockConfiguration(gomock.NewController(t))
c.EXPECT().GrantTypeRefreshTokenSupported().Return(false)
c.EXPECT().GrantTypeTokenExchangeSupported().Return(false)
c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(false)
c.EXPECT().GrantTypeClientCredentialsSupported().Return(false)
c.EXPECT().GrantTypeDeviceCodeSupported().Return(false)
return c
}(),
},
[]oidc.GrantType{
oidc.GrantTypeCode,
oidc.GrantTypeImplicit,
},
},
{
"code, implicit flow, refresh token, token exchange, jwt profile, client_credentials",
args{
func() op.Configuration {
c := mock.NewMockConfiguration(gomock.NewController(t))
c.EXPECT().GrantTypeRefreshTokenSupported().Return(true)
c.EXPECT().GrantTypeTokenExchangeSupported().Return(true)
c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(true)
c.EXPECT().GrantTypeClientCredentialsSupported().Return(true)
c.EXPECT().GrantTypeDeviceCodeSupported().Return(false)
return c
}(),
},
[]oidc.GrantType{
oidc.GrantTypeCode,
oidc.GrantTypeImplicit,
oidc.GrantTypeRefreshToken,
oidc.GrantTypeClientCredentials,
oidc.GrantTypeTokenExchange,
oidc.GrantTypeBearer,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.GrantTypes(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_SubjectTypes(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"none",
args{},
[]string{"public"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.SubjectTypes(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_SigAlgorithms(t *testing.T) {
m := mock.NewMockDiscoverStorage(gomock.NewController(t))
type args struct {
s op.DiscoverStorage
}
tests := []struct {
name string
args args
want []string
}{
{
"",
args{func() op.DiscoverStorage {
m.EXPECT().SignatureAlgorithms(gomock.Any()).Return([]jose.SignatureAlgorithm{jose.RS256}, nil)
return m
}()},
[]string{"RS256"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.SigAlgorithms(context.Background(), tt.args.s)
assert.Equal(t, tt.want, got)
})
}
}
func Test_RequestObjectSigAlgorithms(t *testing.T) {
m := mock.NewMockConfiguration(gomock.NewController(t))
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"not supported, empty",
args{func() op.Configuration {
m.EXPECT().RequestObjectSupported().Return(false)
return m
}()},
nil,
},
{
"supported, empty",
args{func() op.Configuration {
m.EXPECT().RequestObjectSupported().Return(true)
m.EXPECT().RequestObjectSigningAlgorithmsSupported().Return(nil)
return m
}()},
nil,
},
{
"supported, list",
args{func() op.Configuration {
m.EXPECT().RequestObjectSupported().Return(true)
m.EXPECT().RequestObjectSigningAlgorithmsSupported().Return([]string{"RS256"})
return m
}()},
[]string{"RS256"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.RequestObjectSigAlgorithms(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_AuthMethodsTokenEndpoint(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []oidc.AuthMethod
}{
{
"none and basic",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPostSupported().Return(false)
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic},
},
{
"none, basic and post",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPostSupported().Return(true)
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost},
},
{
"none, basic, post and private_key_jwt",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPostSupported().Return(true)
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.AuthMethodsTokenEndpoint(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_TokenSigAlgorithms(t *testing.T) {
m := mock.NewMockConfiguration(gomock.NewController(t))
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"not supported, empty",
args{func() op.Configuration {
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
nil,
},
{
"supported, empty",
args{func() op.Configuration {
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().TokenEndpointSigningAlgorithmsSupported().Return(nil)
return m
}()},
nil,
},
{
"supported, list",
args{func() op.Configuration {
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().TokenEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
return m
}()},
[]string{"RS256"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.TokenSigAlgorithms(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_IntrospectionSigAlgorithms(t *testing.T) {
m := mock.NewMockConfiguration(gomock.NewController(t))
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"not supported, empty",
args{func() op.Configuration {
m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
nil,
},
{
"supported, empty",
args{func() op.Configuration {
m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().IntrospectionEndpointSigningAlgorithmsSupported().Return(nil)
return m
}()},
nil,
},
{
"supported, list",
args{func() op.Configuration {
m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().IntrospectionEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
return m
}()},
[]string{"RS256"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.IntrospectionSigAlgorithms(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_AuthMethodsIntrospectionEndpoint(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []oidc.AuthMethod
}{
{
"basic only",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodBasic},
},
{
"basic and private_key_jwt",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodBasic, oidc.AuthMethodPrivateKeyJWT},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.AuthMethodsIntrospectionEndpoint(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_RevocationSigAlgorithms(t *testing.T) {
m := mock.NewMockConfiguration(gomock.NewController(t))
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"not supported, empty",
args{func() op.Configuration {
m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
nil,
},
{
"supported, empty",
args{func() op.Configuration {
m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().RevocationEndpointSigningAlgorithmsSupported().Return(nil)
return m
}()},
nil,
},
{
"supported, list",
args{func() op.Configuration {
m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(true)
m.EXPECT().RevocationEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
return m
}()},
[]string{"RS256"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.RevocationSigAlgorithms(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_AuthMethodsRevocationEndpoint(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []oidc.AuthMethod
}{
{
"none and basic",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPostSupported().Return(false)
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic},
},
{
"none, basic and post",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPostSupported().Return(true)
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost},
},
{
"none, basic, post and private_key_jwt",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().AuthMethodPostSupported().Return(true)
m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
return m
}()},
[]oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.AuthMethodsRevocationEndpoint(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func TestSupportedClaims(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []string
}{
{
"scopes",
args{},
[]string{
"sub",
"aud",
"exp",
"iat",
"iss",
"auth_time",
"nonce",
"acr",
"amr",
"c_hash",
"at_hash",
"act",
"scopes",
"client_id",
"azp",
"preferred_username",
"name",
"family_name",
"given_name",
"locale",
"email",
"email_verified",
"phone_number",
"phone_number_verified",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.SupportedClaims(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
func Test_CodeChallengeMethods(t *testing.T) {
type args struct {
c op.Configuration
}
tests := []struct {
name string
args args
want []oidc.CodeChallengeMethod
}{
{
"not supported",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().CodeMethodS256Supported().Return(false)
return m
}()},
[]oidc.CodeChallengeMethod{},
},
{
"S256",
args{func() op.Configuration {
m := mock.NewMockConfiguration(gomock.NewController(t))
m.EXPECT().CodeMethodS256Supported().Return(true)
return m
}()},
[]oidc.CodeChallengeMethod{oidc.CodeChallengeMethodS256},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := op.CodeChallengeMethods(tt.args.c)
assert.Equal(t, tt.want, got)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/op/endpoint.go 0000664 0000000 0000000 00000001613 14656014552 0022257 0 ustar 00root root 0000000 0000000 package op
import (
"errors"
"strings"
)
type Endpoint struct {
path string
url string
}
func NewEndpoint(path string) *Endpoint {
return &Endpoint{path: path}
}
func NewEndpointWithURL(path, url string) *Endpoint {
return &Endpoint{path: path, url: url}
}
func (e *Endpoint) Relative() string {
if e == nil {
return ""
}
return relativeEndpoint(e.path)
}
func (e *Endpoint) Absolute(host string) string {
if e == nil {
return ""
}
if e.url != "" {
return e.url
}
return absoluteEndpoint(host, e.path)
}
var ErrNilEndpoint = errors.New("nil endpoint")
func (e *Endpoint) Validate() error {
if e == nil {
return ErrNilEndpoint
}
return nil // TODO:
}
func absoluteEndpoint(host, endpoint string) string {
return strings.TrimSuffix(host, "/") + relativeEndpoint(endpoint)
}
func relativeEndpoint(endpoint string) string {
return "/" + strings.TrimPrefix(endpoint, "/")
}
golang-github-zitadel-oidc-3.27.0/pkg/op/endpoint_test.go 0000664 0000000 0000000 00000003740 14656014552 0023321 0 ustar 00root root 0000000 0000000 package op_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/op"
)
func TestEndpoint_Path(t *testing.T) {
tests := []struct {
name string
e *op.Endpoint
want string
}{
{
"without starting /",
op.NewEndpoint("test"),
"/test",
},
{
"with starting /",
op.NewEndpoint("/test"),
"/test",
},
{
"with url",
op.NewEndpointWithURL("/test", "http://test.com/test"),
"/test",
},
{
"nil",
nil,
"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.e.Relative(); got != tt.want {
t.Errorf("Endpoint.Relative() = %v, want %v", got, tt.want)
}
})
}
}
func TestEndpoint_Absolute(t *testing.T) {
type args struct {
host string
}
tests := []struct {
name string
e *op.Endpoint
args args
want string
}{
{
"no /",
op.NewEndpoint("test"),
args{"https://host"},
"https://host/test",
},
{
"endpoint without /",
op.NewEndpoint("test"),
args{"https://host/"},
"https://host/test",
},
{
"host without /",
op.NewEndpoint("/test"),
args{"https://host"},
"https://host/test",
},
{
"both /",
op.NewEndpoint("/test"),
args{"https://host/"},
"https://host/test",
},
{
"with url",
op.NewEndpointWithURL("test", "https://test.com/test"),
args{"https://host"},
"https://test.com/test",
},
{
"nil",
nil,
args{"https://host"},
"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.e.Absolute(tt.args.host); got != tt.want {
t.Errorf("Endpoint.Absolute() = %v, want %v", got, tt.want)
}
})
}
}
// TODO: impl test
func TestEndpoint_Validate(t *testing.T) {
tests := []struct {
name string
e *op.Endpoint
wantErr error
}{
{
"nil",
nil,
op.ErrNilEndpoint,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.e.Validate()
require.ErrorIs(t, err, tt.wantErr)
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/op/error.go 0000664 0000000 0000000 00000014251 14656014552 0021572 0 ustar 00root root 0000000 0000000 package op
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
type ErrAuthRequest interface {
GetRedirectURI() string
GetResponseType() oidc.ResponseType
GetState() string
}
// LogAuthRequest is an optional interface,
// that allows logging AuthRequest fields.
// If the AuthRequest does not implement this interface,
// no details shall be printed to the logs.
type LogAuthRequest interface {
ErrAuthRequest
slog.LogValuer
}
func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthRequest, err error, authorizer Authorizer) {
e := oidc.DefaultToServerError(err, err.Error())
logger := authorizer.Logger().With("oidc_error", e)
if authReq == nil {
logger.Log(r.Context(), e.LogLevel(), "auth request")
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if logAuthReq, ok := authReq.(LogAuthRequest); ok {
logger = logger.With("auth_request", logAuthReq)
}
if authReq.GetRedirectURI() == "" || e.IsRedirectDisabled() {
logger.Log(r.Context(), e.LogLevel(), "auth request: not redirecting")
http.Error(w, e.Description, http.StatusBadRequest)
return
}
e.State = authReq.GetState()
var responseMode oidc.ResponseMode
if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok {
responseMode = rm.GetResponseMode()
}
url, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), responseMode, e, authorizer.Encoder())
if err != nil {
logger.ErrorContext(r.Context(), "auth response URL", "error", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
logger.Log(r.Context(), e.LogLevel(), "auth request")
http.Redirect(w, r, url, http.StatusFound)
}
func RequestError(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger) {
e := oidc.DefaultToServerError(err, err.Error())
status := http.StatusBadRequest
if e.ErrorType == oidc.InvalidClient {
status = http.StatusUnauthorized
}
logger.Log(r.Context(), e.LogLevel(), "request error", "oidc_error", e)
httphelper.MarshalJSONWithStatus(w, e, status)
}
// TryErrorRedirect tries to handle an error by redirecting a client.
// If this attempt fails, an error is returned that must be returned
// to the client instead.
func TryErrorRedirect(ctx context.Context, authReq ErrAuthRequest, parent error, encoder httphelper.Encoder, logger *slog.Logger) (*Redirect, error) {
e := oidc.DefaultToServerError(parent, parent.Error())
logger = logger.With("oidc_error", e)
if authReq == nil {
logger.Log(ctx, e.LogLevel(), "auth request")
return nil, AsStatusError(e, http.StatusBadRequest)
}
if logAuthReq, ok := authReq.(LogAuthRequest); ok {
logger = logger.With("auth_request", logAuthReq)
}
if authReq.GetRedirectURI() == "" || e.IsRedirectDisabled() {
logger.Log(ctx, e.LogLevel(), "auth request: not redirecting")
return nil, AsStatusError(e, http.StatusBadRequest)
}
e.State = authReq.GetState()
var responseMode oidc.ResponseMode
if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok {
responseMode = rm.GetResponseMode()
}
url, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), responseMode, e, encoder)
if err != nil {
logger.ErrorContext(ctx, "auth response URL", "error", err)
return nil, AsStatusError(err, http.StatusBadRequest)
}
logger.Log(ctx, e.LogLevel(), "auth request redirect", "url", url)
return NewRedirect(url), nil
}
// StatusError wraps an error with a HTTP status code.
// The status code is passed to the handler's writer.
type StatusError struct {
parent error
statusCode int
}
// NewStatusError sets the parent and statusCode to a new StatusError.
// It is recommended for parent to be an [oidc.Error].
//
// Typically implementations should only use this to signal something
// very specific, like an internal server error.
// If a returned error is not a StatusError, the framework
// will set a statusCode based on what the standard specifies,
// which is [http.StatusBadRequest] for most of the time.
// If the error encountered can described clearly with a [oidc.Error],
// do not use this function, as it might break standard rules!
func NewStatusError(parent error, statusCode int) StatusError {
return StatusError{
parent: parent,
statusCode: statusCode,
}
}
// AsStatusError unwraps a StatusError from err
// and returns it unmodified if found.
// If no StatuError was found, a new one is returned
// with statusCode set to it as a default.
func AsStatusError(err error, statusCode int) (target StatusError) {
if errors.As(err, &target) {
return target
}
return NewStatusError(err, statusCode)
}
func (e StatusError) Error() string {
return fmt.Sprintf("%s: %s", http.StatusText(e.statusCode), e.parent.Error())
}
func (e StatusError) Unwrap() error {
return e.parent
}
func (e StatusError) Is(err error) bool {
var target StatusError
if !errors.As(err, &target) {
return false
}
return errors.Is(e.parent, target.parent) &&
e.statusCode == target.statusCode
}
// WriteError asserts for a [StatusError] containing an [oidc.Error].
// If no `StatusError` is found, the status code will default to [http.StatusBadRequest].
// If no `oidc.Error` was found in the parent, the error type defaults to [oidc.ServerError].
// When there was no `StatusError` and the `oidc.Error` is of type `oidc.ServerError`,
// the status code will be set to [http.StatusInternalServerError]
func WriteError(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger) {
var statusError StatusError
if errors.As(err, &statusError) {
writeError(w, r,
oidc.DefaultToServerError(statusError.parent, statusError.parent.Error()),
statusError.statusCode, logger,
)
return
}
statusCode := http.StatusBadRequest
e := oidc.DefaultToServerError(err, err.Error())
if e.ErrorType == oidc.ServerError {
statusCode = http.StatusInternalServerError
}
writeError(w, r, e, statusCode, logger)
}
func writeError(w http.ResponseWriter, r *http.Request, err *oidc.Error, statusCode int, logger *slog.Logger) {
logger.Log(r.Context(), err.LogLevel(), "request error", "oidc_error", err, "status_code", statusCode)
httphelper.MarshalJSONWithStatus(w, err, statusCode)
}
golang-github-zitadel-oidc-3.27.0/pkg/op/error_test.go 0000664 0000000 0000000 00000042370 14656014552 0022634 0 ustar 00root root 0000000 0000000 package op
import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/schema"
)
func TestAuthRequestError(t *testing.T) {
type args struct {
authReq ErrAuthRequest
err error
}
tests := []struct {
name string
args args
wantCode int
wantHeaders map[string]string
wantBody string
wantLog string
}{
{
name: "nil auth request",
args: args{
authReq: nil,
err: io.ErrClosedPipe,
},
wantCode: http.StatusBadRequest,
wantBody: "io: read/write on closed pipe\n",
wantLog: `{
"level":"ERROR",
"msg":"auth request",
"time":"not",
"oidc_error":{
"description":"io: read/write on closed pipe",
"parent":"io: read/write on closed pipe",
"type":"server_error"
}
}`,
},
{
name: "auth request, no redirect URI",
args: args{
authReq: &oidc.AuthRequest{
Scopes: oidc.SpaceDelimitedArray{"a", "b"},
ResponseType: "responseType",
ClientID: "123",
State: "state1",
ResponseMode: oidc.ResponseModeQuery,
},
err: oidc.ErrInteractionRequired().WithDescription("sign in"),
},
wantCode: http.StatusBadRequest,
wantBody: "sign in\n",
wantLog: `{
"level":"WARN",
"msg":"auth request: not redirecting",
"time":"not",
"auth_request":{
"client_id":"123",
"redirect_uri":"",
"response_type":"responseType",
"scopes":"a b"
},
"oidc_error":{
"description":"sign in",
"type":"interaction_required"
}
}`,
},
{
name: "auth request, redirect disabled",
args: args{
authReq: &oidc.AuthRequest{
Scopes: oidc.SpaceDelimitedArray{"a", "b"},
ResponseType: "responseType",
ClientID: "123",
RedirectURI: "http://example.com/callback",
State: "state1",
ResponseMode: oidc.ResponseModeQuery,
},
err: oidc.ErrInvalidRequestRedirectURI().WithDescription("oops"),
},
wantCode: http.StatusBadRequest,
wantBody: "oops\n",
wantLog: `{
"level":"WARN",
"msg":"auth request: not redirecting",
"time":"not",
"auth_request":{
"client_id":"123",
"redirect_uri":"http://example.com/callback",
"response_type":"responseType",
"scopes":"a b"
},
"oidc_error":{
"description":"oops",
"type":"invalid_request",
"redirect_disabled":true
}
}`,
},
{
name: "auth request, url parse error",
args: args{
authReq: &oidc.AuthRequest{
Scopes: oidc.SpaceDelimitedArray{"a", "b"},
ResponseType: "responseType",
ClientID: "123",
RedirectURI: "can't parse this!\n",
State: "state1",
ResponseMode: oidc.ResponseModeQuery,
},
err: oidc.ErrInteractionRequired().WithDescription("sign in"),
},
wantCode: http.StatusBadRequest,
wantBody: "ErrorType=server_error Parent=parse \"can't parse this!\\n\": net/url: invalid control character in URL\n",
wantLog: `{
"level":"ERROR",
"msg":"auth response URL",
"time":"not",
"auth_request":{
"client_id":"123",
"redirect_uri":"can't parse this!\n",
"response_type":"responseType",
"scopes":"a b"
},
"error":{
"type":"server_error",
"parent":"parse \"can't parse this!\\n\": net/url: invalid control character in URL"
},
"oidc_error":{
"description":"sign in",
"type":"interaction_required"
}
}`,
},
{
name: "auth request redirect",
args: args{
authReq: &oidc.AuthRequest{
Scopes: oidc.SpaceDelimitedArray{"a", "b"},
ResponseType: "responseType",
ClientID: "123",
RedirectURI: "http://example.com/callback",
State: "state1",
ResponseMode: oidc.ResponseModeQuery,
},
err: oidc.ErrInteractionRequired().WithDescription("sign in"),
},
wantCode: http.StatusFound,
wantHeaders: map[string]string{"Location": "http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1"},
wantLog: `{
"level":"WARN",
"msg":"auth request",
"time":"not",
"auth_request":{
"client_id":"123",
"redirect_uri":"http://example.com/callback",
"response_type":"responseType",
"scopes":"a b"
},
"oidc_error":{
"description":"sign in",
"type":"interaction_required"
}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logOut := new(strings.Builder)
authorizer := &Provider{
encoder: schema.NewEncoder(),
logger: slog.New(
slog.NewJSONHandler(logOut, &slog.HandlerOptions{
Level: slog.LevelInfo,
}).WithAttrs([]slog.Attr{slog.String("time", "not")}),
),
}
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/path", nil)
AuthRequestError(w, r, tt.args.authReq, tt.args.err, authorizer)
res := w.Result()
defer res.Body.Close()
assert.Equal(t, tt.wantCode, res.StatusCode)
for key, wantHeader := range tt.wantHeaders {
gotHeader := res.Header.Get(key)
assert.Equalf(t, wantHeader, gotHeader, "header %q", key)
}
gotBody, err := io.ReadAll(res.Body)
require.NoError(t, err, "read result body")
assert.Equal(t, tt.wantBody, string(gotBody), "result body")
gotLog := logOut.String()
t.Log(gotLog)
assert.JSONEq(t, tt.wantLog, gotLog, "log output")
})
}
}
func TestRequestError(t *testing.T) {
tests := []struct {
name string
err error
wantCode int
wantBody string
wantLog string
}{
{
name: "server error",
err: io.ErrClosedPipe,
wantCode: http.StatusBadRequest,
wantBody: `{"error":"server_error", "error_description":"io: read/write on closed pipe"}`,
wantLog: `{
"level":"ERROR",
"msg":"request error",
"time":"not",
"oidc_error":{
"parent":"io: read/write on closed pipe",
"description":"io: read/write on closed pipe",
"type":"server_error"}
}`,
},
{
name: "invalid client",
err: oidc.ErrInvalidClient().WithDescription("not good"),
wantCode: http.StatusUnauthorized,
wantBody: `{"error":"invalid_client", "error_description":"not good"}`,
wantLog: `{
"level":"WARN",
"msg":"request error",
"time":"not",
"oidc_error":{
"description":"not good",
"type":"invalid_client"}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logOut := new(strings.Builder)
logger := slog.New(
slog.NewJSONHandler(logOut, &slog.HandlerOptions{
Level: slog.LevelInfo,
}).WithAttrs([]slog.Attr{slog.String("time", "not")}),
)
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/path", nil)
RequestError(w, r, tt.err, logger)
res := w.Result()
defer res.Body.Close()
assert.Equal(t, tt.wantCode, res.StatusCode, "status code")
gotBody, err := io.ReadAll(res.Body)
require.NoError(t, err, "read result body")
assert.JSONEq(t, tt.wantBody, string(gotBody), "result body")
gotLog := logOut.String()
t.Log(gotLog)
assert.JSONEq(t, tt.wantLog, gotLog, "log output")
})
}
}
func TestTryErrorRedirect(t *testing.T) {
type args struct {
ctx context.Context
authReq ErrAuthRequest
parent error
}
tests := []struct {
name string
args args
want *Redirect
wantErr error
wantLog string
}{
{
name: "nil auth request",
args: args{
ctx: context.Background(),
authReq: nil,
parent: io.ErrClosedPipe,
},
wantErr: NewStatusError(io.ErrClosedPipe, http.StatusBadRequest),
wantLog: `{
"level":"ERROR",
"msg":"auth request",
"time":"not",
"oidc_error":{
"description":"io: read/write on closed pipe",
"parent":"io: read/write on closed pipe",
"type":"server_error"
}
}`,
},
{
name: "auth request, no redirect URI",
args: args{
ctx: context.Background(),
authReq: &oidc.AuthRequest{
Scopes: oidc.SpaceDelimitedArray{"a", "b"},
ResponseType: "responseType",
ClientID: "123",
State: "state1",
ResponseMode: oidc.ResponseModeQuery,
},
parent: oidc.ErrInteractionRequired().WithDescription("sign in"),
},
wantErr: NewStatusError(oidc.ErrInteractionRequired().WithDescription("sign in"), http.StatusBadRequest),
wantLog: `{
"level":"WARN",
"msg":"auth request: not redirecting",
"time":"not",
"auth_request":{
"client_id":"123",
"redirect_uri":"",
"response_type":"responseType",
"scopes":"a b"
},
"oidc_error":{
"description":"sign in",
"type":"interaction_required"
}
}`,
},
{
name: "auth request, redirect disabled",
args: args{
ctx: context.Background(),
authReq: &oidc.AuthRequest{
Scopes: oidc.SpaceDelimitedArray{"a", "b"},
ResponseType: "responseType",
ClientID: "123",
RedirectURI: "http://example.com/callback",
State: "state1",
ResponseMode: oidc.ResponseModeQuery,
},
parent: oidc.ErrInvalidRequestRedirectURI().WithDescription("oops"),
},
wantErr: NewStatusError(oidc.ErrInvalidRequestRedirectURI().WithDescription("oops"), http.StatusBadRequest),
wantLog: `{
"level":"WARN",
"msg":"auth request: not redirecting",
"time":"not",
"auth_request":{
"client_id":"123",
"redirect_uri":"http://example.com/callback",
"response_type":"responseType",
"scopes":"a b"
},
"oidc_error":{
"description":"oops",
"type":"invalid_request",
"redirect_disabled":true
}
}`,
},
{
name: "auth request, url parse error",
args: args{
ctx: context.Background(),
authReq: &oidc.AuthRequest{
Scopes: oidc.SpaceDelimitedArray{"a", "b"},
ResponseType: "responseType",
ClientID: "123",
RedirectURI: "can't parse this!\n",
State: "state1",
ResponseMode: oidc.ResponseModeQuery,
},
parent: oidc.ErrInteractionRequired().WithDescription("sign in"),
},
wantErr: func() error {
//lint:ignore SA1007 just recreating the error for testing
_, err := url.Parse("can't parse this!\n")
err = oidc.ErrServerError().WithParent(err)
return NewStatusError(err, http.StatusBadRequest)
}(),
wantLog: `{
"level":"ERROR",
"msg":"auth response URL",
"time":"not",
"auth_request":{
"client_id":"123",
"redirect_uri":"can't parse this!\n",
"response_type":"responseType",
"scopes":"a b"
},
"error":{
"type":"server_error",
"parent":"parse \"can't parse this!\\n\": net/url: invalid control character in URL"
},
"oidc_error":{
"description":"sign in",
"type":"interaction_required"
}
}`,
},
{
name: "auth request redirect",
args: args{
ctx: context.Background(),
authReq: &oidc.AuthRequest{
Scopes: oidc.SpaceDelimitedArray{"a", "b"},
ResponseType: "responseType",
ClientID: "123",
RedirectURI: "http://example.com/callback",
State: "state1",
ResponseMode: oidc.ResponseModeQuery,
},
parent: oidc.ErrInteractionRequired().WithDescription("sign in"),
},
want: &Redirect{
URL: "http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1",
},
wantLog: `{
"level":"WARN",
"msg":"auth request redirect",
"time":"not",
"auth_request":{
"client_id":"123",
"redirect_uri":"http://example.com/callback",
"response_type":"responseType",
"scopes":"a b"
},
"oidc_error":{
"description":"sign in",
"type":"interaction_required"
},
"url":"http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1"
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logOut := new(strings.Builder)
logger := slog.New(
slog.NewJSONHandler(logOut, &slog.HandlerOptions{
Level: slog.LevelInfo,
}).WithAttrs([]slog.Attr{slog.String("time", "not")}),
)
encoder := schema.NewEncoder()
got, err := TryErrorRedirect(tt.args.ctx, tt.args.authReq, tt.args.parent, encoder, logger)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
gotLog := logOut.String()
t.Log(gotLog)
assert.JSONEq(t, tt.wantLog, gotLog, "log output")
})
}
}
func TestNewStatusError(t *testing.T) {
err := NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError)
want := "Internal Server Error: io: read/write on closed pipe"
got := fmt.Sprint(err)
assert.Equal(t, want, got)
}
func TestAsStatusError(t *testing.T) {
type args struct {
err error
statusCode int
}
tests := []struct {
name string
args args
want string
}{
{
name: "already status error",
args: args{
err: NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError),
statusCode: http.StatusBadRequest,
},
want: "Internal Server Error: io: read/write on closed pipe",
},
{
name: "oidc error",
args: args{
err: oidc.ErrAcrInvalid,
statusCode: http.StatusBadRequest,
},
want: "Bad Request: acr is invalid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := AsStatusError(tt.args.err, tt.args.statusCode)
got := fmt.Sprint(err)
assert.Equal(t, tt.want, got)
})
}
}
func TestStatusError_Unwrap(t *testing.T) {
err := NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError)
require.ErrorIs(t, err, io.ErrClosedPipe)
}
func TestStatusError_Is(t *testing.T) {
type args struct {
err error
}
tests := []struct {
name string
args args
want bool
}{
{
name: "nil error",
args: args{err: nil},
want: false,
},
{
name: "other error",
args: args{err: io.EOF},
want: false,
},
{
name: "other parent",
args: args{err: NewStatusError(io.EOF, http.StatusInternalServerError)},
want: false,
},
{
name: "other status",
args: args{err: NewStatusError(io.ErrClosedPipe, http.StatusInsufficientStorage)},
want: false,
},
{
name: "same",
args: args{err: NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError)},
want: true,
},
{
name: "wrapped",
args: args{err: fmt.Errorf("wrap: %w", NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError))},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError)
if got := e.Is(tt.args.err); got != tt.want {
t.Errorf("StatusError.Is() = %v, want %v", got, tt.want)
}
})
}
}
func TestWriteError(t *testing.T) {
tests := []struct {
name string
err error
wantStatus int
wantBody string
wantLog string
}{
{
name: "not a status or oidc error",
err: io.ErrClosedPipe,
wantStatus: http.StatusInternalServerError,
wantBody: `{
"error":"server_error",
"error_description":"io: read/write on closed pipe"
}`,
wantLog: `{
"level":"ERROR",
"msg":"request error",
"oidc_error":{
"description":"io: read/write on closed pipe",
"parent":"io: read/write on closed pipe",
"type":"server_error"
},
"status_code":500,
"time":"not"
}`,
},
{
name: "status error w/o oidc",
err: NewStatusError(io.ErrClosedPipe, http.StatusInternalServerError),
wantStatus: http.StatusInternalServerError,
wantBody: `{
"error":"server_error",
"error_description":"io: read/write on closed pipe"
}`,
wantLog: `{
"level":"ERROR",
"msg":"request error",
"oidc_error":{
"description":"io: read/write on closed pipe",
"parent":"io: read/write on closed pipe",
"type":"server_error"
},
"status_code":500,
"time":"not"
}`,
},
{
name: "oidc error w/o status",
err: oidc.ErrInvalidRequest().WithDescription("oops"),
wantStatus: http.StatusBadRequest,
wantBody: `{
"error":"invalid_request",
"error_description":"oops"
}`,
wantLog: `{
"level":"WARN",
"msg":"request error",
"oidc_error":{
"description":"oops",
"type":"invalid_request"
},
"status_code":400,
"time":"not"
}`,
},
{
name: "status with oidc error",
err: NewStatusError(
oidc.ErrUnauthorizedClient().WithDescription("oops"),
http.StatusUnauthorized,
),
wantStatus: http.StatusUnauthorized,
wantBody: `{
"error":"unauthorized_client",
"error_description":"oops"
}`,
wantLog: `{
"level":"WARN",
"msg":"request error",
"oidc_error":{
"description":"oops",
"type":"unauthorized_client"
},
"status_code":401,
"time":"not"
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logOut := new(strings.Builder)
logger := slog.New(
slog.NewJSONHandler(logOut, &slog.HandlerOptions{
Level: slog.LevelInfo,
}).WithAttrs([]slog.Attr{slog.String("time", "not")}),
)
r := httptest.NewRequest("GET", "/target", nil)
w := httptest.NewRecorder()
WriteError(w, r, tt.err, logger)
res := w.Result()
assert.Equal(t, tt.wantStatus, res.StatusCode, "status code")
gotBody, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.JSONEq(t, tt.wantBody, string(gotBody), "body")
assert.JSONEq(t, tt.wantLog, logOut.String())
})
}
}
golang-github-zitadel-oidc-3.27.0/pkg/op/form_post.html.tmpl 0000664 0000000 0000000 00000001373 14656014552 0023764 0 ustar 00root root 0000000 0000000