Compare commits
1 commit
main
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
|
c43d0e7295 |
156 changed files with 1912 additions and 11356 deletions
|
@ -1,57 +0,0 @@
|
||||||
name: Bug Report
|
|
||||||
description: "Create a bug report to help us improve ZITADEL. Click [here](https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#product-management) to see how we process your issue."
|
|
||||||
title: "[Bug]: "
|
|
||||||
labels: ["bug"]
|
|
||||||
type: Bug
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for taking the time to fill out this bug report!
|
|
||||||
- type: checkboxes
|
|
||||||
id: preflight
|
|
||||||
attributes:
|
|
||||||
label: Preflight Checklist
|
|
||||||
options:
|
|
||||||
- label:
|
|
||||||
I could not find a solution in the documentation, the existing issues or discussions
|
|
||||||
required: true
|
|
||||||
- label:
|
|
||||||
I have joined the [ZITADEL chat](https://zitadel.com/chat)
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: Version
|
|
||||||
description: Which version of the OIDC library are you using.
|
|
||||||
- type: textarea
|
|
||||||
id: impact
|
|
||||||
attributes:
|
|
||||||
label: Describe the problem caused by this bug
|
|
||||||
description: A clear and concise description of the problem you have and what the bug is.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: reproduce
|
|
||||||
attributes:
|
|
||||||
label: To reproduce
|
|
||||||
description: Steps to reproduce the behaviour
|
|
||||||
placeholder: |
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: Screenshots
|
|
||||||
description: If applicable, add screenshots to help explain your problem.
|
|
||||||
- type: textarea
|
|
||||||
id: expected
|
|
||||||
attributes:
|
|
||||||
label: Expected behavior
|
|
||||||
description: A clear and concise description of what you expected to happen.
|
|
||||||
placeholder: As a [type of user], I want [some goal] so that [some reason].
|
|
||||||
- type: textarea
|
|
||||||
id: additional
|
|
||||||
attributes:
|
|
||||||
label: Additional Context
|
|
||||||
description: Please add any other infos that could be useful.
|
|
|
@ -1,31 +0,0 @@
|
||||||
name: 📄 Documentation
|
|
||||||
description: Create an issue for missing or wrong documentation.
|
|
||||||
labels: ["docs"]
|
|
||||||
type: task
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for taking the time to fill out this issue.
|
|
||||||
- type: checkboxes
|
|
||||||
id: preflight
|
|
||||||
attributes:
|
|
||||||
label: Preflight Checklist
|
|
||||||
options:
|
|
||||||
- label:
|
|
||||||
I could not find a solution in the existing issues, docs, nor discussions
|
|
||||||
required: true
|
|
||||||
- label:
|
|
||||||
I have joined the [ZITADEL chat](https://zitadel.com/chat)
|
|
||||||
- type: textarea
|
|
||||||
id: docs
|
|
||||||
attributes:
|
|
||||||
label: Describe the docs your are missing or that are wrong
|
|
||||||
placeholder: As a [type of user], I want [some goal] so that [some reason].
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: additional
|
|
||||||
attributes:
|
|
||||||
label: Additional Context
|
|
||||||
description: Please add any other infos that could be useful.
|
|
|
@ -1,55 +0,0 @@
|
||||||
name: 🛠️ Improvement
|
|
||||||
description: "Create an new issue for an improvment in ZITADEL"
|
|
||||||
labels: ["enhancement"]
|
|
||||||
type: enhancement
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for taking the time to fill out this proposal / feature reqeust
|
|
||||||
- type: checkboxes
|
|
||||||
id: preflight
|
|
||||||
attributes:
|
|
||||||
label: Preflight Checklist
|
|
||||||
options:
|
|
||||||
- label:
|
|
||||||
I could not find a solution in the existing issues, docs, nor discussions
|
|
||||||
required: true
|
|
||||||
- label:
|
|
||||||
I have joined the [ZITADEL chat](https://zitadel.com/chat)
|
|
||||||
- type: textarea
|
|
||||||
id: problem
|
|
||||||
attributes:
|
|
||||||
label: Describe your problem
|
|
||||||
description: Please describe your problem this improvement is supposed to solve.
|
|
||||||
placeholder: Describe the problem you have
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: solution
|
|
||||||
attributes:
|
|
||||||
label: Describe your ideal solution
|
|
||||||
description: Which solution do you propose?
|
|
||||||
placeholder: As a [type of user], I want [some goal] so that [some reason].
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: Version
|
|
||||||
description: Which version of the OIDC Library are you using.
|
|
||||||
- type: dropdown
|
|
||||||
id: environment
|
|
||||||
attributes:
|
|
||||||
label: Environment
|
|
||||||
description: How do you use ZITADEL?
|
|
||||||
options:
|
|
||||||
- ZITADEL Cloud
|
|
||||||
- Self-hosted
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: additional
|
|
||||||
attributes:
|
|
||||||
label: Additional Context
|
|
||||||
description: Please add any other infos that could be useful.
|
|
|
@ -1,16 +0,0 @@
|
||||||
### Definition of Ready
|
|
||||||
|
|
||||||
- [ ] I am happy with the code
|
|
||||||
- [ ] Short description of the feature/issue is added in the pr description
|
|
||||||
- [ ] PR is linked to the corresponding user story
|
|
||||||
- [ ] Acceptance criteria are met
|
|
||||||
- [ ] All open todos and follow ups are defined in a new ticket and justified
|
|
||||||
- [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented.
|
|
||||||
- [ ] No debug or dead code
|
|
||||||
- [ ] My code has no repetitions
|
|
||||||
- [ ] Critical parts are tested automatically
|
|
||||||
- [ ] Where possible E2E tests are implemented
|
|
||||||
- [ ] Documentation/examples are up-to-date
|
|
||||||
- [ ] All non-functional requirements are met
|
|
||||||
- [ ] Functionality of the acceptance criteria is checked manually on the dev system.
|
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
name: Add new issues to product management project
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
add-to-project:
|
|
||||||
name: Add issue and community pr to project
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: add issue
|
|
||||||
uses: actions/add-to-project@v1.0.2
|
|
||||||
if: ${{ github.event_name == 'issues' }}
|
|
||||||
with:
|
|
||||||
# You can target a repository in a different organization
|
|
||||||
# to the issue
|
|
||||||
project-url: https://github.com/orgs/zitadel/projects/2
|
|
||||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
|
||||||
- uses: tspascoal/get-user-teams-membership@v3
|
|
||||||
id: checkUserMember
|
|
||||||
if: github.actor != 'dependabot[bot]'
|
|
||||||
with:
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
|
||||||
- name: add pr
|
|
||||||
uses: actions/add-to-project@v1.0.2
|
|
||||||
if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}}
|
|
||||||
with:
|
|
||||||
# You can target a repository in a different organization
|
|
||||||
# to the issue
|
|
||||||
project-url: https://github.com/orgs/zitadel/projects/2
|
|
||||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
|
||||||
- uses: actions-ecosystem/action-add-labels@v1.1.3
|
|
||||||
if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'staff')}}
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
|
||||||
labels: |
|
|
||||||
os-contribution
|
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
name: 🐛 Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: 🚀 Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
|
@ -9,16 +9,6 @@ updates:
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: chore
|
prefix: chore
|
||||||
include: scope
|
include: scope
|
||||||
- package-ecosystem: gomod
|
|
||||||
target-branch: "2.12.x"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
time: '04:00'
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
commit-message:
|
|
||||||
prefix: chore
|
|
||||||
include: scope
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
|
@ -29,7 +29,7 @@ jobs:
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v2
|
||||||
# Override language selection by uncommenting this and choosing your languages
|
# Override language selection by uncommenting this and choosing your languages
|
||||||
with:
|
with:
|
||||||
languages: go
|
languages: go
|
||||||
|
@ -37,7 +37,7 @@ jobs:
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
@ -51,4 +51,4 @@ jobs:
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v2
|
18
.github/workflows/issue.yml
vendored
Normal file
18
.github/workflows/issue.yml
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
name: Add new issues to product management project
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-to-project:
|
||||||
|
name: Add issue to project
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/add-to-project@v0.4.1
|
||||||
|
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 }}
|
|
@ -2,7 +2,6 @@ name: Release
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "2.11.x"
|
|
||||||
- main
|
- main
|
||||||
- next
|
- next
|
||||||
tags-ignore:
|
tags-ignore:
|
||||||
|
@ -14,34 +13,33 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
go: ['1.23', '1.24']
|
go: ['1.18', '1.19', '1.20']
|
||||||
name: Go ${{ matrix.go }} test
|
name: Go ${{ matrix.go }} test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
- run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/...
|
- run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/...
|
||||||
- uses: codecov/codecov-action@v5.4.3
|
- uses: codecov/codecov-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
file: ./profile.cov
|
file: ./profile.cov
|
||||||
name: codecov-go
|
name: codecov-go
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [test]
|
needs: [test]
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }}
|
if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Source checkout
|
- name: Source checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Semantic Release
|
- name: Semantic Release
|
||||||
uses: cycjimmy/semantic-release-action@v4
|
uses: cycjimmy/semantic-release-action@v3
|
||||||
with:
|
with:
|
||||||
dry_run: false
|
dry_run: false
|
||||||
semantic_version: 18.0.1
|
semantic_version: 18.0.1
|
|
@ -1,6 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
branches: [
|
branches: [
|
||||||
{name: "2.11.x"},
|
|
||||||
{name: "main"},
|
{name: "main"},
|
||||||
{name: "next", prerelease: true},
|
{name: "next", prerelease: true},
|
||||||
],
|
],
|
||||||
|
|
119
README.md
119
README.md
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
[](https://github.com/semantic-release/semantic-release)
|
[](https://github.com/semantic-release/semantic-release)
|
||||||
[](https://github.com/zitadel/oidc/actions)
|
[](https://github.com/zitadel/oidc/actions)
|
||||||
[](https://pkg.go.dev/github.com/zitadel/oidc/v3)
|
[](https://pkg.go.dev/github.com/zitadel/oidc)
|
||||||
[](https://github.com/zitadel/oidc/blob/master/LICENSE)
|
[](https://github.com/zitadel/oidc/blob/master/LICENSE)
|
||||||
[](https://github.com/zitadel/oidc/releases)
|
[](https://github.com/zitadel/oidc/releases)
|
||||||
[](https://goreportcard.com/report/github.com/zitadel/oidc/v3)
|
[](https://goreportcard.com/report/github.com/zitadel/oidc)
|
||||||
[](https://codecov.io/gh/zitadel/oidc)
|
[](https://codecov.io/gh/zitadel/oidc)
|
||||||
|
|
||||||
[](https://openid.net/certification/)
|

|
||||||
|
|
||||||
## What Is It
|
## What Is It
|
||||||
|
|
||||||
|
@ -21,10 +21,9 @@ Whenever possible we tried to reuse / extend existing packages like `OAuth2 for
|
||||||
## Basic Overview
|
## Basic Overview
|
||||||
|
|
||||||
The most important packages of the library:
|
The most important packages of the library:
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
/pkg
|
/pkg
|
||||||
/client clients using the OP for retrieving, exchanging and verifying tokens
|
/client clients using the OP for retrieving, exchanging and verifying tokens
|
||||||
/rp definition and implementation of an OIDC Relying Party (client)
|
/rp definition and implementation of an OIDC Relying Party (client)
|
||||||
/rs definition and implementation of an OAuth Resource Server (API)
|
/rs definition and implementation of an OAuth Resource Server (API)
|
||||||
/op definition and implementation of an OIDC OpenID Provider (server)
|
/op definition and implementation of an OIDC OpenID Provider (server)
|
||||||
|
@ -38,10 +37,6 @@ The most important packages of the library:
|
||||||
/server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI
|
/server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
### Semver
|
|
||||||
|
|
||||||
This package uses [semver](https://semver.org/) for [releases](https://github.com/zitadel/oidc/releases). Major releases ship breaking changes. Starting with the `v2` to `v3` increment we provide an [upgrade guide](UPGRADING.md) to ease migration to a newer version.
|
|
||||||
|
|
||||||
## How To Use It
|
## How To Use It
|
||||||
|
|
||||||
Check the `/example` folder where example code for different scenarios is located.
|
Check the `/example` folder where example code for different scenarios is located.
|
||||||
|
@ -49,90 +44,54 @@ Check the `/example` folder where example code for different scenarios is locate
|
||||||
```bash
|
```bash
|
||||||
# start oidc op server
|
# start oidc op server
|
||||||
# oidc discovery http://localhost:9998/.well-known/openid-configuration
|
# oidc discovery http://localhost:9998/.well-known/openid-configuration
|
||||||
go run github.com/zitadel/oidc/v3/example/server
|
go run github.com/zitadel/oidc/v2/example/server
|
||||||
# start oidc web client (in a new terminal)
|
# start oidc web client (in a new terminal)
|
||||||
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app
|
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app
|
||||||
```
|
```
|
||||||
|
|
||||||
- open http://localhost:9999/login in your browser
|
- open http://localhost:9999/login in your browser
|
||||||
- you will be redirected to op server and the login UI
|
- you will be redirected to op server and the login UI
|
||||||
- login with user `test-user@localhost` and password `verysecure`
|
- login with user `test-user@localhost` and password `verysecure`
|
||||||
- the OP will redirect you to the client app, which displays the user info
|
- the OP will redirect you to the client app, which displays the user info
|
||||||
|
|
||||||
for the dynamic issuer, just start it with:
|
for the dynamic issuer, just start it with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run github.com/zitadel/oidc/v3/example/server/dynamic
|
go run github.com/zitadel/oidc/v2/example/server/dynamic
|
||||||
```
|
```
|
||||||
|
|
||||||
the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with:
|
the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app
|
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`)
|
> Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`)
|
||||||
|
|
||||||
### Server configuration
|
|
||||||
|
|
||||||
Example server allows extra configuration using environment variables and could be used for end to
|
|
||||||
end testing of your services.
|
|
||||||
|
|
||||||
| Name | Format | Description |
|
|
||||||
| ------------ | -------------------------------- | ------------------------------------- |
|
|
||||||
| PORT | Number between 1 and 65535 | OIDC listen port |
|
|
||||||
| REDIRECT_URI | Comma-separated URIs | List of allowed redirect URIs |
|
|
||||||
| USERS_FILE | Path to json in local filesystem | Users with their data and credentials |
|
|
||||||
|
|
||||||
Here is json equivalent for one of the default users
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id2": {
|
|
||||||
"ID": "id2",
|
|
||||||
"Username": "test-user2",
|
|
||||||
"Password": "verysecure",
|
|
||||||
"FirstName": "Test",
|
|
||||||
"LastName": "User2",
|
|
||||||
"Email": "test-user2@zitadel.ch",
|
|
||||||
"EmailVerified": true,
|
|
||||||
"Phone": "",
|
|
||||||
"PhoneVerified": false,
|
|
||||||
"PreferredLanguage": "DE",
|
|
||||||
"IsAdmin": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
| | Relying party | OpenID Provider | Specification |
|
| | Relying party | OpenID Provider | Specification |
|
||||||
| -------------------- | ------------- | --------------- | -------------------------------------------- |
|
| -------------------- | ------------- | --------------- | ----------------------------------------- |
|
||||||
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] |
|
| 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] |
|
| 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] |
|
| 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] |
|
| Client Credentials | not yet | yes | OpenID Connect Core 1.0, [Section 9][4] |
|
||||||
| Refresh Token | yes | yes | OpenID Connect Core 1.0, [Section 12][5] |
|
| Refresh Token | yes | yes | OpenID Connect Core 1.0, [Section 12][5] |
|
||||||
| Discovery | yes | yes | OpenID Connect [Discovery][6] 1.0 |
|
| Discovery | yes | yes | OpenID Connect [Discovery][6] 1.0 |
|
||||||
| JWT Profile | yes | yes | [RFC 7523][7] |
|
| JWT Profile | yes | yes | [RFC 7523][7] |
|
||||||
| PKCE | yes | yes | [RFC 7636][8] |
|
| PKCE | yes | yes | [RFC 7636][8] |
|
||||||
| Token Exchange | yes | yes | [RFC 8693][9] |
|
| Token Exchange | yes | yes | [RFC 8693][9] |
|
||||||
| Device Authorization | yes | yes | [RFC 8628][10] |
|
| Device Authorization | yes | yes | [RFC 8628][10] |
|
||||||
| mTLS | not yet | not yet | [RFC 8705][11] |
|
| mTLS | not yet | not yet | [RFC 8705][11] |
|
||||||
| Back-Channel Logout | not yet | yes | OpenID Connect [Back-Channel Logout][12] 1.0 |
|
|
||||||
|
|
||||||
[1]: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth "3.1. Authentication using the Authorization Code Flow"
|
[1]: <https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth> "3.1. Authentication using the Authorization Code Flow"
|
||||||
[2]: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth "3.2. Authentication using the Implicit Flow"
|
[2]: <https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth> "3.2. Authentication using the Implicit Flow"
|
||||||
[3]: https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth "3.3. Authentication using the Hybrid Flow"
|
[3]: <https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth> "3.3. Authentication using the Hybrid Flow"
|
||||||
[4]: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication "9. Client Authentication"
|
[4]: <https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication> "9. Client Authentication"
|
||||||
[5]: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens "12. Using Refresh Tokens"
|
[5]: <https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens> "12. Using Refresh Tokens"
|
||||||
[6]: https://openid.net/specs/openid-connect-discovery-1_0.html "OpenID Connect Discovery 1.0 incorporating errata set 1"
|
[6]: <https://openid.net/specs/openid-connect-discovery-1_0.html> "OpenID Connect Discovery 1.0 incorporating errata set 1"
|
||||||
[7]: https://www.rfc-editor.org/rfc/rfc7523.html "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
|
[7]: <https://www.rfc-editor.org/rfc/rfc7523.html> "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
|
||||||
[8]: https://www.rfc-editor.org/rfc/rfc7636.html "Proof Key for Code Exchange by OAuth Public Clients"
|
[8]: <https://www.rfc-editor.org/rfc/rfc7636.html> "Proof Key for Code Exchange by OAuth Public Clients"
|
||||||
[9]: https://www.rfc-editor.org/rfc/rfc8693.html "OAuth 2.0 Token Exchange"
|
[9]: <https://www.rfc-editor.org/rfc/rfc8693.html> "OAuth 2.0 Token Exchange"
|
||||||
[10]: https://www.rfc-editor.org/rfc/rfc8628.html "OAuth 2.0 Device Authorization Grant"
|
[10]: <https://www.rfc-editor.org/rfc/rfc8628.html> "OAuth 2.0 Device Authorization Grant"
|
||||||
[11]: https://www.rfc-editor.org/rfc/rfc8705.html "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
|
[11]: <https://www.rfc-editor.org/rfc/rfc8705.html> "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
|
||||||
[12]: https://openid.net/specs/openid-connect-backchannel-1_0.html "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1"
|
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
|
@ -151,14 +110,15 @@ For your convenience you can find the relevant guides linked below.
|
||||||
|
|
||||||
## Supported Go Versions
|
## Supported Go Versions
|
||||||
|
|
||||||
For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:).
|
For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:).
|
||||||
Versions that also build are marked with :warning:.
|
Versions that also build are marked with :warning:.
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| <1.23 | :x: |
|
| <1.18 | :x: |
|
||||||
| 1.23 | :white_check_mark: |
|
| 1.18 | :warning: |
|
||||||
| 1.24 | :white_check_mark: |
|
| 1.19 | :white_check_mark: |
|
||||||
|
| 1.20 | :white_check_mark: |
|
||||||
|
|
||||||
## Why another library
|
## Why another library
|
||||||
|
|
||||||
|
@ -189,4 +149,5 @@ Unless required by applicable law or agreed to in writing, software distributed
|
||||||
AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
|
AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
|
||||||
language governing permissions and limitations under the License.
|
language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
[^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892
|
[^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892
|
||||||
|
|
49
SECURITY.md
49
SECURITY.md
|
@ -1,20 +1,43 @@
|
||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
Please refer to the security policy [on zitadel/zitadel](https://github.com/zitadel/zitadel/blob/main/SECURITY.md) which is applicable for all open source repositories of our organization.
|
At ZITADEL we are extremely grateful for security aware people that disclose vulnerabilities to us and the open source community. All reports will be investigated by our team.
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
We currently support the following version of the OIDC framework:
|
After the initial Release the following version support will apply
|
||||||
|
|
||||||
| Version | Supported | Branch | Details |
|
| Version | Supported |
|
||||||
| -------- | ------------------ | ----------- | ------------------------------------ |
|
| ------- | ------------------ |
|
||||||
| 0.x.x | :x: | | not maintained |
|
| 0.x.x | :x: |
|
||||||
| <2.11 | :x: | | not maintained |
|
| 1.x.x | :white_check_mark: |
|
||||||
| 2.11.x | :lock: :warning: | [2.11.x][1] | security only, [community effort][2] |
|
| 2.x.x | :white_check_mark: (not released) |
|
||||||
| 3.x.x | :heavy_check_mark: | [main][3] | supported |
|
|
||||||
| 4.0.0-xx | :white_check_mark: | [next][4] | [development branch] |
|
|
||||||
|
|
||||||
[1]: https://github.com/zitadel/oidc/tree/2.11.x
|
## Reporting a vulnerability
|
||||||
[2]: https://github.com/zitadel/oidc/discussions/458
|
|
||||||
[3]: https://github.com/zitadel/oidc/tree/main
|
To file a incident, please disclose by email to security@zitadel.com with the security details.
|
||||||
[4]: https://github.com/zitadel/oidc/tree/next
|
|
||||||
|
At the moment GPG encryption is no yet supported, however you may sign your message at will.
|
||||||
|
|
||||||
|
### When should I report a vulnerability
|
||||||
|
|
||||||
|
* You think you discovered a ...
|
||||||
|
* ... potential security vulnerability in the SDK
|
||||||
|
* ... vulnerability in another project that this SDK bases on
|
||||||
|
* For projects with their own vulnerability reporting and disclosure process, please report it directly there
|
||||||
|
|
||||||
|
### When should I NOT report a vulnerability
|
||||||
|
|
||||||
|
* You need help applying security related updates
|
||||||
|
* Your issue is not security related
|
||||||
|
|
||||||
|
## Security Vulnerability Response
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
## Public Disclosure
|
||||||
|
|
||||||
|
All accepted and mitigated vulnerabilities will be published on the [Github Security Page](https://github.com/zitadel/oidc/security/advisories)
|
||||||
|
|
||||||
|
### Timing
|
||||||
|
|
||||||
|
We think it is crucial to publish advisories `ASAP` as mitigations are ready. But due to the unknown nature of the disclosures the time frame can range from 7 to 90 days.
|
||||||
|
|
370
UPGRADING.md
370
UPGRADING.md
|
@ -1,370 +0,0 @@
|
||||||
# Upgrading
|
|
||||||
|
|
||||||
All commands are executed from the root of the project that imports oidc packages.
|
|
||||||
`sed` commands are created with **GNU sed** in mind and might need alternate syntax
|
|
||||||
on non-GNU systems, such as MacOS.
|
|
||||||
Alternatively, GNU sed can be installed on such systems. (`coreutils` package?).
|
|
||||||
|
|
||||||
## V2 to V3
|
|
||||||
|
|
||||||
**TL;DR** at the [bottom](#full-script) of this chapter is a full `sed` script
|
|
||||||
containing all automatic steps at once.
|
|
||||||
|
|
||||||
|
|
||||||
As first steps we will:
|
|
||||||
1. Download the latest v3 module;
|
|
||||||
2. Replace imports in all Go files;
|
|
||||||
3. Tidy the module file;
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go get -u github.com/zitadel/oidc/v3
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/github\.com\/zitadel\/oidc\/v2/github.com\/zitadel\/oidc\/v3/g'
|
|
||||||
go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
### global
|
|
||||||
|
|
||||||
#### go-jose package
|
|
||||||
|
|
||||||
`gopkg.in/square/go-jose.v2` import has been changed to `github.com/go-jose/go-jose/v3`.
|
|
||||||
That means that the imported types are also changed and imports need to be adapted.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/gopkg.in\/square\/go-jose\.v2/github.com\/go-jose\/go-jose\/v3/g'
|
|
||||||
go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
### op
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "github.com/zitadel/oidc/v3/pkg/op"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Logger
|
|
||||||
|
|
||||||
This version of OIDC adds logging to the framework. For this we use the new Go standard library `log/slog`. (Until v3.12.0 we used `x/exp/slog`).
|
|
||||||
Mostly OIDC will use error level logs where it's returning an error through a HTTP handler. OIDC errors that are user facing don't carry much context, also for security reasons. With logging we are now able to print the error context, so that developers can more easily find the source of their issues. Previously we just discarded such context.
|
|
||||||
|
|
||||||
Most users of the OP package with the storage interface will not experience breaking changes. However if you use `RequestError()` directly in your code, you now need to give it a `Logger` as final argument.
|
|
||||||
|
|
||||||
The `OpenIDProvider` and sub-interfaces like `Authorizer` and `Exchanger` got a `Logger()` method to return the configured logger. This logger is in turn used by `AuthRequestError()`. You configure the logger with the `WithLogger()` for the `Provider`. By default the `slog.Default()` is used.
|
|
||||||
|
|
||||||
We also provide a new optional interface: [`LogAuthRequest`](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/op#LogAuthRequest). If an `AuthRequest` implements this interface, it is completely passed into the logger after an error. Its `LogValue()` will be used by `slog` to print desired fields. This allows omitting sensitive fields you wish not no print. If the interface is not implemented, no `AuthRequest` details will ever be printed.
|
|
||||||
|
|
||||||
#### Server interface
|
|
||||||
|
|
||||||
We've added a new [`Server`](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/op#Server) interface. This interface is experimental and subject to change. See [issue 440](https://github.com/zitadel/oidc/issues/440) for the motivation and discussion around this new interface.
|
|
||||||
Usage of the new interface is not required, but may be used for advanced scenarios when working with the `Storage` interface isn't the optimal solution for your app (like we experienced in [Zitadel](https://github.com/zitadel/zitadel)).
|
|
||||||
|
|
||||||
#### AuthRequestError
|
|
||||||
|
|
||||||
`AuthRequestError` now takes the complete `Authorizer` as final argument, instead of only the encoder.
|
|
||||||
This is to facilitate the use of the `Logger` as described above.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/\bAuthRequestError(w, r, authReq, err, authorizer.Encoder())/AuthRequestError(w, r, authReq, err, authorizer)/g'
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: the sed regex might not find all uses if the local variables of the passed arguments use different names.
|
|
||||||
|
|
||||||
#### AccessTokenVerifier
|
|
||||||
|
|
||||||
`AccessTokenVerifier` interface has become a struct type. `NewAccessTokenVerifier` now returns a pointer to `AccessTokenVerifier`.
|
|
||||||
Variable and struct fields declarations need to be changed from `op.AccessTokenVerifier` to `*op.AccessTokenVerifier`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/\bop\.AccessTokenVerifier\b/*op.AccessTokenVerifier/g'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JWTProfileVerifier
|
|
||||||
|
|
||||||
`JWTProfileVerifier` interface has become a struct type. `NewJWTProfileVerifier` now returns a pointer to `JWTProfileVerifier`.
|
|
||||||
Variable and struct fields declarations need to be changed from `op.JWTProfileVerifier` to `*op.JWTProfileVerifier`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/\bop\.JWTProfileVerifier\b/*op.JWTProfileVerifier/g'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### IDTokenHintVerifier
|
|
||||||
|
|
||||||
`IDTokenHintVerifier` interface has become a struct type. `NewIDTokenHintVerifier` now returns a pointer to `IDTokenHintVerifier`.
|
|
||||||
Variable and struct fields declarations need to be changed from `op.IDTokenHintVerifier` to `*op.IDTokenHintVerifier`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/\bop\.IDTokenHintVerifier\b/*op.IDTokenHintVerifier/g'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ParseRequestObject
|
|
||||||
|
|
||||||
`ParseRequestObject` no longer returns `*oidc.AuthRequest` as it already operates on the pointer for the passed `authReq` argument. As such the argument and the return value were the same pointer. Callers can just use the original `*oidc.AuthRequest` now.
|
|
||||||
|
|
||||||
#### Endpoint Configuration
|
|
||||||
|
|
||||||
`Endpoint`s returned from `Configuration` interface methods are now pointers. Usually, `op.Provider` is the main implementation of the `Configuration` interface. However, if a custom implementation is used, you should be able to update it using the following:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/AuthorizationEndpoint() Endpoint/AuthorizationEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/TokenEndpoint() Endpoint/TokenEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/IntrospectionEndpoint() Endpoint/IntrospectionEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/UserinfoEndpoint() Endpoint/UserinfoEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/RevocationEndpoint() Endpoint/RevocationEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/EndSessionEndpoint() Endpoint/EndSessionEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/KeysEndpoint() Endpoint/KeysEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/DeviceAuthorizationEndpoint() Endpoint/DeviceAuthorizationEndpoint() *Endpoint/g'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### CreateDiscoveryConfig
|
|
||||||
|
|
||||||
`CreateDiscoveryConfig` now takes a context as first argument. The following adds `context.TODO()` to the function:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/op\.CreateDiscoveryConfig(/op.CreateDiscoveryConfig(context.TODO(), /g'
|
|
||||||
```
|
|
||||||
|
|
||||||
It now takes the issuer out of the context using the [`IssuerFromContext`](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/op#IssuerFromContext) functionality,
|
|
||||||
instead of the `config.IssuerFromRequest()` method.
|
|
||||||
|
|
||||||
#### CreateRouter
|
|
||||||
|
|
||||||
`CreateRouter` now returns a `chi.Router` instead of `*mux.Router`.
|
|
||||||
Usually this function is called when the Provider is constructed and not by package consumers.
|
|
||||||
However if your project does call this function directly, manual update of the code is required.
|
|
||||||
|
|
||||||
#### DeviceAuthorizationStorage
|
|
||||||
|
|
||||||
`DeviceAuthorizationStorage` dropped the following methods:
|
|
||||||
|
|
||||||
- `GetDeviceAuthorizationByUserCode`
|
|
||||||
- `CompleteDeviceAuthorization`
|
|
||||||
- `DenyDeviceAuthorization`
|
|
||||||
|
|
||||||
These methods proved not to be required from a library point of view.
|
|
||||||
Implementations of a device authorization flow may take care of these calls in a way they see fit.
|
|
||||||
|
|
||||||
#### AuthorizeCodeChallenge
|
|
||||||
|
|
||||||
The `AuthorizeCodeChallenge` function now only takes the `CodeVerifier` argument, instead of the complete `*oidc.AccessTokenRequest`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/op\.AuthorizeCodeChallenge(tokenReq/op.AuthorizeCodeChallenge(tokenReq.CodeVerifier/g'
|
|
||||||
```
|
|
||||||
|
|
||||||
### client
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "github.com/zitadel/oidc/v3/pkg/client"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Context
|
|
||||||
|
|
||||||
All client calls now take a context as first argument. The following adds `context.TODO()` to all the affected functions:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/client\.Discover(/client.Discover(context.TODO(), /g' \
|
|
||||||
-e 's/client\.CallTokenEndpoint(/client.CallTokenEndpoint(context.TODO(), /g' \
|
|
||||||
-e 's/client\.CallEndSessionEndpoint(/client.CallEndSessionEndpoint(context.TODO(), /g' \
|
|
||||||
-e 's/client\.CallRevokeEndpoint(/client.CallRevokeEndpoint(context.TODO(), /g' \
|
|
||||||
-e 's/client\.CallTokenExchangeEndpoint(/client.CallTokenExchangeEndpoint(context.TODO(), /g' \
|
|
||||||
-e 's/client\.CallDeviceAuthorizationEndpoint(/client.CallDeviceAuthorizationEndpoint(context.TODO(), /g' \
|
|
||||||
-e 's/client\.JWTProfileExchange(/client.JWTProfileExchange(context.TODO(), /g'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### keyFile type
|
|
||||||
|
|
||||||
The `keyFile` struct type is now exported a `KeyFile` and returned by the `ConfigFromKeyFile` and `ConfigFromKeyFileData`. No changes are needed on the caller's side.
|
|
||||||
|
|
||||||
### client/profile
|
|
||||||
|
|
||||||
The package now defines a new interface `TokenSource` which compliments the `oauth2.TokenSource` with a `TokenCtx` method, so that a context can be explicitly added on each call. Users can migrate to the new method when they whish.
|
|
||||||
|
|
||||||
`NewJWTProfileTokenSource` now takes a context as first argument, so do the related `NewJWTProfileTokenSourceFromKeyFile` and `NewJWTProfileTokenSourceFromKeyFileData`. The context is used for the Discovery request.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/profile\.NewJWTProfileTokenSource(/profile.NewJWTProfileTokenSource(context.TODO(), /g' \
|
|
||||||
-e 's/profile\.NewJWTProfileTokenSourceFromKeyFileData(/profile.NewJWTProfileTokenSourceFromKeyFileData(context.TODO(), /g' \
|
|
||||||
-e 's/profile\.NewJWTProfileTokenSourceFromKeyFile(/profile.NewJWTProfileTokenSourceFromKeyFile(context.TODO(), /g'
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### client/rp
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "github.com/zitadel/oidc/v3/pkg/client/rs"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Discover
|
|
||||||
|
|
||||||
The `Discover` function has been removed. Use `client.Discover` instead.
|
|
||||||
|
|
||||||
#### Context
|
|
||||||
|
|
||||||
Most `rp` functions now require a context as first argument. The following adds `context.TODO()` to the function that have no additional changes. Functions with more complex changes are documented below.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/rp\.NewRelyingPartyOIDC(/rp.NewRelyingPartyOIDC(context.TODO(), /g' \
|
|
||||||
-e 's/rp\.EndSession(/rp.EndSession(context.TODO(), /g' \
|
|
||||||
-e 's/rp\.RevokeToken(/rp.RevokeToken(context.TODO(), /g' \
|
|
||||||
-e 's/rp\.DeviceAuthorization(/rp.DeviceAuthorization(context.TODO(), /g'
|
|
||||||
```
|
|
||||||
|
|
||||||
Remember to replace `context.TODO()` with a context that is applicable for your app, where possible.
|
|
||||||
|
|
||||||
#### RefreshAccessToken
|
|
||||||
|
|
||||||
1. Renamed to `RefreshTokens`;
|
|
||||||
2. A context must be passed;
|
|
||||||
3. An `*oidc.Tokens` object is now returned, which included an ID Token if it was returned by the server;
|
|
||||||
4. The function is now generic and requires a type argument for the `IDTokenClaims` implementation inside the returned `oidc.Tokens` object;
|
|
||||||
|
|
||||||
For most use cases `*oidc.IDTokenClaims` can be used as type argument. A custom implementation of `oidc.IDClaims` can be used if type-safe access to custom claims is required.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/rp\.RefreshAccessToken(/rp.RefreshTokens[*oidc.IDTokenClaims](context.TODO(), /g'
|
|
||||||
```
|
|
||||||
|
|
||||||
Users that called `tokens.Extra("id_token").(string)` and a subsequent `VerifyTokens` to get the claims, no longer need to do this. The ID token is verified (when present) by `RefreshTokens` already.
|
|
||||||
|
|
||||||
|
|
||||||
#### Userinfo
|
|
||||||
|
|
||||||
1. A context must be passed as first argument;
|
|
||||||
2. The function is now generic and requires a type argument for the returned user info object;
|
|
||||||
|
|
||||||
For most use cases `*oidc.UserInfo` can be used a type argument. A [custom implementation](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/client/rp#example-Userinfo-Custom) of `rp.SubjectGetter` can be used if type-safe access to custom claims is required.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/rp\.Userinfo(/rp.Userinfo[*oidc.UserInfo](context.TODO(), /g'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### UserinfoCallback
|
|
||||||
|
|
||||||
`UserinfoCallback` has an additional type argument fot the `UserInfo` object. Typically the type argument can be inferred by the compiler, by the function that is passed. The actual code update cannot be done by a simple `sed` script and depends on how the caller implemented the function.
|
|
||||||
|
|
||||||
|
|
||||||
#### IDTokenVerifier
|
|
||||||
|
|
||||||
`IDTokenVerifier` interface has become a struct type. `NewIDTokenVerifier` now returns a pointer to `IDTokenVerifier`.
|
|
||||||
Variable and struct fields declarations need to be changed from `rp.IDTokenVerifier` to `*rp.AccessTokenVerifier`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/\brp\.IDTokenVerifier\b/*rp.IDTokenVerifier/g'
|
|
||||||
```
|
|
||||||
|
|
||||||
### client/rs
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "github.com/zitadel/oidc/v3/pkg/client/rs"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### NewResourceServer
|
|
||||||
|
|
||||||
The `NewResourceServerClientCredentials` and `NewResourceServerJWTProfile` constructor functions now take a context as first argument.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/rs\.NewResourceServerClientCredentials(/rs.NewResourceServerClientCredentials(context.TODO(), /g' \
|
|
||||||
-e 's/rs\.NewResourceServerJWTProfile(/rs.NewResourceServerJWTProfile(context.TODO(), /g'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Introspect
|
|
||||||
|
|
||||||
`Introspect` is now generic and requires a type argument for the returned introspection response. For most use cases `*oidc.IntrospectionResponse` can be used as type argument. Any other response type if type-safe access to [custom claims](https://pkg.go.dev/github.com/zitadel/oidc/v3/pkg/client/rs#example-Introspect-Custom) is required.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/rs\.Introspect(/rs.Introspect[*oidc.IntrospectionResponse](/g'
|
|
||||||
```
|
|
||||||
|
|
||||||
### client/tokenexchange
|
|
||||||
|
|
||||||
The `TokenExchanger` constructor functions `NewTokenExchanger` and `NewTokenExchangerClientCredentials` now take a context as first argument.
|
|
||||||
As well as the `ExchangeToken` function.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/tokenexchange\.NewTokenExchanger(/tokenexchange.NewTokenExchanger(context.TODO(), /g' \
|
|
||||||
-e 's/tokenexchange\.NewTokenExchangerClientCredentials(/tokenexchange.NewTokenExchangerClientCredentials(context.TODO(), /g' \
|
|
||||||
-e 's/tokenexchange\.ExchangeToken(/tokenexchange.ExchangeToken(context.TODO(), /g'
|
|
||||||
```
|
|
||||||
|
|
||||||
### oidc
|
|
||||||
|
|
||||||
#### SpaceDelimitedArray
|
|
||||||
|
|
||||||
The `SpaceDelimitedArray` type's `Encode()` function has been renamed to `String()` so it implements the `fmt.Stringer` interface. If the `Encode` method was called by a package consumer, it should be changed manually.
|
|
||||||
|
|
||||||
#### Verifier
|
|
||||||
|
|
||||||
The `Verifier` interface as been changed into a struct type. The struct type is aliased in the `op` and `rp` packages for the specific token use cases. See the relevant section above.
|
|
||||||
|
|
||||||
### Full script
|
|
||||||
|
|
||||||
For the courageous this is the full `sed` script which combines all the steps described above.
|
|
||||||
It should migrate most of the code in a repository to a more-or-less compilable state,
|
|
||||||
using defaults such as `context.TODO()` where possible.
|
|
||||||
|
|
||||||
Warnings:
|
|
||||||
- Again, this is written for **GNU sed** not the posix variant.
|
|
||||||
- Assumes imports that use the package names, not aliases.
|
|
||||||
- Do this on a project with version control (eg Git), that allows you to rollback if things went wrong.
|
|
||||||
- The script has been tested on the [ZITADEL](https://github.com/zitadel/zitadel) project, but we do not use all affected symbols. Parts of the script are mere guesswork.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go get -u github.com/zitadel/oidc/v3
|
|
||||||
find . -type f -name '*.go' | xargs sed -i \
|
|
||||||
-e 's/github\.com\/zitadel\/oidc\/v2/github.com\/zitadel\/oidc\/v3/g' \
|
|
||||||
-e 's/gopkg.in\/square\/go-jose\.v2/github.com\/go-jose\/go-jose\/v3/g' \
|
|
||||||
-e 's/\bAuthRequestError(w, r, authReq, err, authorizer.Encoder())/AuthRequestError(w, r, authReq, err, authorizer)/g' \
|
|
||||||
-e 's/\bop\.AccessTokenVerifier\b/*op.AccessTokenVerifier/g' \
|
|
||||||
-e 's/\bop\.JWTProfileVerifier\b/*op.JWTProfileVerifier/g' \
|
|
||||||
-e 's/\bop\.IDTokenHintVerifier\b/*op.IDTokenHintVerifier/g' \
|
|
||||||
-e 's/AuthorizationEndpoint() Endpoint/AuthorizationEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/TokenEndpoint() Endpoint/TokenEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/IntrospectionEndpoint() Endpoint/IntrospectionEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/UserinfoEndpoint() Endpoint/UserinfoEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/RevocationEndpoint() Endpoint/RevocationEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/EndSessionEndpoint() Endpoint/EndSessionEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/KeysEndpoint() Endpoint/KeysEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/DeviceAuthorizationEndpoint() Endpoint/DeviceAuthorizationEndpoint() *Endpoint/g' \
|
|
||||||
-e 's/op\.CreateDiscoveryConfig(/op.CreateDiscoveryConfig(context.TODO(), /g' \
|
|
||||||
-e 's/op\.AuthorizeCodeChallenge(tokenReq/op.AuthorizeCodeChallenge(tokenReq.CodeVerifier/g' \
|
|
||||||
-e 's/client\.Discover(/client.Discover(context.TODO(), /g' \
|
|
||||||
-e 's/client\.CallTokenEndpoint(/client.CallTokenEndpoint(context.TODO(), /g' \
|
|
||||||
-e 's/client\.CallEndSessionEndpoint(/client.CallEndSessionEndpoint(context.TODO(), /g' \
|
|
||||||
-e 's/client\.CallRevokeEndpoint(/client.CallRevokeEndpoint(context.TODO(), /g' \
|
|
||||||
-e 's/client\.CallTokenExchangeEndpoint(/client.CallTokenExchangeEndpoint(context.TODO(), /g' \
|
|
||||||
-e 's/client\.CallDeviceAuthorizationEndpoint(/client.CallDeviceAuthorizationEndpoint(context.TODO(), /g' \
|
|
||||||
-e 's/client\.JWTProfileExchange(/client.JWTProfileExchange(context.TODO(), /g' \
|
|
||||||
-e 's/profile\.NewJWTProfileTokenSource(/profile.NewJWTProfileTokenSource(context.TODO(), /g' \
|
|
||||||
-e 's/profile\.NewJWTProfileTokenSourceFromKeyFileData(/profile.NewJWTProfileTokenSourceFromKeyFileData(context.TODO(), /g' \
|
|
||||||
-e 's/profile\.NewJWTProfileTokenSourceFromKeyFile(/profile.NewJWTProfileTokenSourceFromKeyFile(context.TODO(), /g' \
|
|
||||||
-e 's/rp\.NewRelyingPartyOIDC(/rp.NewRelyingPartyOIDC(context.TODO(), /g' \
|
|
||||||
-e 's/rp\.EndSession(/rp.EndSession(context.TODO(), /g' \
|
|
||||||
-e 's/rp\.RevokeToken(/rp.RevokeToken(context.TODO(), /g' \
|
|
||||||
-e 's/rp\.DeviceAuthorization(/rp.DeviceAuthorization(context.TODO(), /g' \
|
|
||||||
-e 's/rp\.RefreshAccessToken(/rp.RefreshTokens[*oidc.IDTokenClaims](context.TODO(), /g' \
|
|
||||||
-e 's/rp\.Userinfo(/rp.Userinfo[*oidc.UserInfo](context.TODO(), /g' \
|
|
||||||
-e 's/\brp\.IDTokenVerifier\b/*rp.IDTokenVerifier/g' \
|
|
||||||
-e 's/rs\.NewResourceServerClientCredentials(/rs.NewResourceServerClientCredentials(context.TODO(), /g' \
|
|
||||||
-e 's/rs\.NewResourceServerJWTProfile(/rs.NewResourceServerJWTProfile(context.TODO(), /g' \
|
|
||||||
-e 's/rs\.Introspect(/rs.Introspect[*oidc.IntrospectionResponse](/g' \
|
|
||||||
-e 's/tokenexchange\.NewTokenExchanger(/tokenexchange.NewTokenExchanger(context.TODO(), /g' \
|
|
||||||
-e 's/tokenexchange\.NewTokenExchangerClientCredentials(/tokenexchange.NewTokenExchangerClientCredentials(context.TODO(), /g' \
|
|
||||||
-e 's/tokenexchange\.ExchangeToken(/tokenexchange.ExchangeToken(context.TODO(), /g'
|
|
||||||
go mod tidy
|
|
||||||
```
|
|
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
@ -10,11 +9,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/gorilla/mux"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs"
|
"github.com/zitadel/oidc/v2/pkg/client/rs"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -28,12 +27,12 @@ func main() {
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
issuer := os.Getenv("ISSUER")
|
issuer := os.Getenv("ISSUER")
|
||||||
|
|
||||||
provider, err := rs.NewResourceServerFromKeyFile(context.TODO(), issuer, keyPath)
|
provider, err := rs.NewResourceServerFromKeyFile(issuer, keyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("error creating provider %s", err.Error())
|
logrus.Fatalf("error creating provider %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
router := chi.NewRouter()
|
router := mux.NewRouter()
|
||||||
|
|
||||||
// public url accessible without any authorization
|
// public url accessible without any authorization
|
||||||
// will print `OK` and current timestamp
|
// will print `OK` and current timestamp
|
||||||
|
@ -48,7 +47,7 @@ func main() {
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := rs.Introspect[*oidc.IntrospectionResponse](r.Context(), provider, token)
|
resp, err := rs.Introspect(r.Context(), provider, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
|
@ -69,14 +68,14 @@ func main() {
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := rs.Introspect[*oidc.IntrospectionResponse](r.Context(), provider, token)
|
resp, err := rs.Introspect(r.Context(), provider, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requestedClaim := chi.URLParam(r, "claim")
|
params := mux.Vars(r)
|
||||||
requestedValue := chi.URLParam(r, "value")
|
requestedClaim := params["claim"]
|
||||||
|
requestedValue := params["value"]
|
||||||
value, ok := resp.Claims[requestedClaim].(string)
|
value, ok := resp.Claims[requestedClaim].(string)
|
||||||
if !ok || value == "" || value != requestedValue {
|
if !ok || value == "" || value != requestedValue {
|
||||||
http.Error(w, "claim does not match", http.StatusForbidden)
|
http.Error(w, "claim does not match", http.StatusForbidden)
|
||||||
|
|
|
@ -1,23 +1,19 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"github.com/zitadel/logging"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -32,31 +28,13 @@ func main() {
|
||||||
issuer := os.Getenv("ISSUER")
|
issuer := os.Getenv("ISSUER")
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
||||||
responseMode := os.Getenv("RESPONSE_MODE")
|
|
||||||
|
|
||||||
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
|
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
|
||||||
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
||||||
|
|
||||||
logger := slog.New(
|
|
||||||
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
||||||
AddSource: true,
|
|
||||||
Level: slog.LevelDebug,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: time.Minute,
|
|
||||||
}
|
|
||||||
// enable outgoing request logging
|
|
||||||
logging.EnableHTTPClient(client,
|
|
||||||
logging.WithClientGroup("client"),
|
|
||||||
)
|
|
||||||
|
|
||||||
options := []rp.Option{
|
options := []rp.Option{
|
||||||
rp.WithCookieHandler(cookieHandler),
|
rp.WithCookieHandler(cookieHandler),
|
||||||
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
|
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
|
||||||
rp.WithHTTPClient(client),
|
|
||||||
rp.WithLogger(logger),
|
|
||||||
rp.WithSigningAlgsFromDiscovery(),
|
|
||||||
}
|
}
|
||||||
if clientSecret == "" {
|
if clientSecret == "" {
|
||||||
options = append(options, rp.WithPKCE(cookieHandler))
|
options = append(options, rp.WithPKCE(cookieHandler))
|
||||||
|
@ -65,10 +43,7 @@ func main() {
|
||||||
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
|
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// One can add a logger to the context,
|
provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...)
|
||||||
// pre-defining log attributes as required.
|
|
||||||
ctx := logging.ToContext(context.TODO(), logger)
|
|
||||||
provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, redirectURI, scopes, options...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("error creating provider %s", err.Error())
|
logrus.Fatalf("error creating provider %s", err.Error())
|
||||||
}
|
}
|
||||||
|
@ -79,37 +54,20 @@ func main() {
|
||||||
return uuid.New().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.
|
// register the AuthURLHandler at your preferred path.
|
||||||
// the AuthURLHandler creates the auth request and redirects the user to the auth server.
|
// 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.
|
// including state handling with secure cookie and the possibility to use PKCE.
|
||||||
// Prompts can optionally be set to inform the server of
|
// Prompts can optionally be set to inform the server of
|
||||||
// any messages that need to be prompted back to the user.
|
// any messages that need to be prompted back to the user.
|
||||||
http.Handle("/login", rp.AuthURLHandler(
|
http.Handle("/login", rp.AuthURLHandler(state, provider, rp.WithPromptURLParam("Welcome back!")))
|
||||||
state,
|
|
||||||
provider,
|
|
||||||
urlOptions...,
|
|
||||||
))
|
|
||||||
|
|
||||||
// for demonstration purposes the returned userinfo response is written as JSON object onto response
|
// for demonstration purposes the returned userinfo response is written as JSON object onto response
|
||||||
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*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)
|
data, err := json.Marshal(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("content-type", "application/json")
|
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,22 +118,8 @@ func main() {
|
||||||
//
|
//
|
||||||
// http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider))
|
// http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider))
|
||||||
|
|
||||||
// simple counter for request IDs
|
|
||||||
var counter atomic.Int64
|
|
||||||
// enable incomming request logging
|
|
||||||
mw := logging.Middleware(
|
|
||||||
logging.WithLogger(logger),
|
|
||||||
logging.WithGroup("server"),
|
|
||||||
logging.WithIDFunc(func() slog.Attr {
|
|
||||||
return slog.Int64("id", counter.Add(1))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
lis := fmt.Sprintf("127.0.0.1:%s", port)
|
lis := fmt.Sprintf("127.0.0.1:%s", port)
|
||||||
logger.Info("server listening, press ctrl+c to stop", "addr", lis)
|
logrus.Infof("listening on http://%s/", lis)
|
||||||
err = http.ListenAndServe(lis, mw(http.DefaultServeMux))
|
logrus.Info("press ctrl+c to stop")
|
||||||
if err != http.ErrServerClosed {
|
logrus.Fatal(http.ListenAndServe(lis, nil))
|
||||||
logger.Error("server terminated", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,3 @@
|
||||||
// Command device is an example Oauth2 Device Authorization Grant app.
|
|
||||||
// It creates a new Device Authorization request on the Issuer and then polls for tokens.
|
|
||||||
// The user is then prompted to visit a URL and enter the user code.
|
|
||||||
// Or, the complete URL can be used instead to omit manual entry.
|
|
||||||
// In practice then can be a "magic link" in the form or a QR.
|
|
||||||
//
|
|
||||||
// The following environment variables are used for configuration:
|
|
||||||
//
|
|
||||||
// ISSUER: URL to the OP, required.
|
|
||||||
// CLIENT_ID: ID of the application, required.
|
|
||||||
// CLIENT_SECRET: Secret to authenticate the app using basic auth. Only required if the OP expects this type of authentication.
|
|
||||||
// KEY_PATH: Path to a private key file, used to for JWT authentication of the App. Only required if the OP expects this type of authentication.
|
|
||||||
// SCOPES: Scopes of the Authentication Request. Optional.
|
|
||||||
//
|
|
||||||
// Basic usage:
|
|
||||||
//
|
|
||||||
// cd example/client/device
|
|
||||||
// export ISSUER="http://localhost:9000" CLIENT_ID="246048465824634593@demo"
|
|
||||||
//
|
|
||||||
// Get an Access Token:
|
|
||||||
//
|
|
||||||
// SCOPES="email profile" go run .
|
|
||||||
//
|
|
||||||
// Get an Access Token and ID Token:
|
|
||||||
//
|
|
||||||
// SCOPES="email profile openid" go run .
|
|
||||||
//
|
|
||||||
// Get an Access Token and Refresh Token
|
|
||||||
//
|
|
||||||
// SCOPES="email profile offline_access" go run .
|
|
||||||
//
|
|
||||||
// Get Access, Refresh and ID Tokens:
|
|
||||||
//
|
|
||||||
// SCOPES="email profile offline_access openid" go run .
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -45,8 +11,8 @@ import (
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -73,13 +39,13 @@ func main() {
|
||||||
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
|
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, "", scopes, options...)
|
provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, "", scopes, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("error creating provider %s", err.Error())
|
logrus.Fatalf("error creating provider %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Info("starting device authorization flow")
|
logrus.Info("starting device authorization flow")
|
||||||
resp, err := rp.DeviceAuthorization(ctx, scopes, provider, nil)
|
resp, err := rp.DeviceAuthorization(scopes, provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -91,5 +57,5 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
logrus.Infof("successfully obtained token: %#v", token)
|
logrus.Infof("successfully obtained token: %v", token)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,10 @@ import (
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
githubOAuth "golang.org/x/oauth2/github"
|
githubOAuth "golang.org/x/oauth2/github"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp/cli"
|
"github.com/zitadel/oidc/v2/pkg/client/rp/cli"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
"github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/profile"
|
"github.com/zitadel/oidc/v2/pkg/client/profile"
|
||||||
)
|
)
|
||||||
|
|
||||||
var client = http.DefaultClient
|
var client = http.DefaultClient
|
||||||
|
@ -25,7 +25,7 @@ func main() {
|
||||||
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
||||||
|
|
||||||
if keyPath != "" {
|
if keyPath != "" {
|
||||||
ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(context.TODO(), issuer, keyPath, scopes)
|
ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath, scopes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("error creating token source %s", err.Error())
|
logrus.Fatalf("error creating token source %s", err.Error())
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ func main() {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(context.TODO(), issuer, key, scopes)
|
ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(issuer, key, scopes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -125,7 +125,7 @@ func main() {
|
||||||
testURL := r.Form.Get("url")
|
testURL := r.Form.Get("url")
|
||||||
var data struct {
|
var data struct {
|
||||||
URL string
|
URL string
|
||||||
Response any
|
Response interface{}
|
||||||
}
|
}
|
||||||
if testURL != "" {
|
if testURL != "" {
|
||||||
data.URL = testURL
|
data.URL = testURL
|
||||||
|
@ -149,7 +149,7 @@ func main() {
|
||||||
logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil))
|
logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func callExampleEndpoint(client *http.Client, testURL string) (any, error) {
|
func callExampleEndpoint(client *http.Client, testURL string) (interface{}, error) {
|
||||||
req, err := http.NewRequest("GET", testURL, nil)
|
req, err := http.NewRequest("GET", testURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// default port for the http server to run
|
|
||||||
DefaultIssuerPort = "9998"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Port string
|
|
||||||
RedirectURI []string
|
|
||||||
UsersFile string
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromEnvVars loads configuration parameters from environment variables.
|
|
||||||
// If there is no such variable defined, then use default values.
|
|
||||||
func FromEnvVars(defaults *Config) *Config {
|
|
||||||
if defaults == nil {
|
|
||||||
defaults = &Config{}
|
|
||||||
}
|
|
||||||
cfg := &Config{
|
|
||||||
Port: defaults.Port,
|
|
||||||
RedirectURI: defaults.RedirectURI,
|
|
||||||
UsersFile: defaults.UsersFile,
|
|
||||||
}
|
|
||||||
if value, ok := os.LookupEnv("PORT"); ok {
|
|
||||||
cfg.Port = value
|
|
||||||
}
|
|
||||||
if value, ok := os.LookupEnv("USERS_FILE"); ok {
|
|
||||||
cfg.UsersFile = value
|
|
||||||
}
|
|
||||||
if value, ok := os.LookupEnv("REDIRECT_URI"); ok {
|
|
||||||
cfg.RedirectURI = strings.Split(value, ",")
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFromEnvVars(t *testing.T) {
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
env map[string]string
|
|
||||||
defaults *Config
|
|
||||||
want *Config
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no vars, no default values",
|
|
||||||
env: map[string]string{},
|
|
||||||
want: &Config{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no vars, only defaults",
|
|
||||||
env: map[string]string{},
|
|
||||||
defaults: &Config{
|
|
||||||
Port: "6666",
|
|
||||||
UsersFile: "/default/user/path",
|
|
||||||
RedirectURI: []string{"re", "direct", "uris"},
|
|
||||||
},
|
|
||||||
want: &Config{
|
|
||||||
Port: "6666",
|
|
||||||
UsersFile: "/default/user/path",
|
|
||||||
RedirectURI: []string{"re", "direct", "uris"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "overriding default values",
|
|
||||||
env: map[string]string{
|
|
||||||
"PORT": "1234",
|
|
||||||
"USERS_FILE": "/path/to/users",
|
|
||||||
"REDIRECT_URI": "http://redirect/redirect",
|
|
||||||
},
|
|
||||||
defaults: &Config{
|
|
||||||
Port: "6666",
|
|
||||||
UsersFile: "/default/user/path",
|
|
||||||
RedirectURI: []string{"re", "direct", "uris"},
|
|
||||||
},
|
|
||||||
want: &Config{
|
|
||||||
Port: "1234",
|
|
||||||
UsersFile: "/path/to/users",
|
|
||||||
RedirectURI: []string{"http://redirect/redirect"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple redirect uris",
|
|
||||||
env: map[string]string{
|
|
||||||
"REDIRECT_URI": "http://host_1,http://host_2,http://host_3",
|
|
||||||
},
|
|
||||||
want: &Config{
|
|
||||||
RedirectURI: []string{
|
|
||||||
"http://host_1", "http://host_2", "http://host_3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
for k, v := range tc.env {
|
|
||||||
os.Setenv(k, v)
|
|
||||||
}
|
|
||||||
cfg := FromEnvVars(tc.defaults)
|
|
||||||
if fmt.Sprint(cfg) != fmt.Sprint(tc.want) {
|
|
||||||
t.Errorf("Expected FromEnvVars()=%q, but got %q", tc.want, cfg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -43,7 +43,7 @@ var (
|
||||||
|
|
||||||
type login struct {
|
type login struct {
|
||||||
authenticate authenticate
|
authenticate authenticate
|
||||||
router chi.Router
|
router *mux.Router
|
||||||
callback func(context.Context, string) string
|
callback func(context.Context, string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,9 +57,9 @@ func NewLogin(authenticate authenticate, callback func(context.Context, string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) {
|
func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) {
|
||||||
l.router = chi.NewRouter()
|
l.router = mux.NewRouter()
|
||||||
l.router.Get("/username", l.loginHandler)
|
l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler)
|
||||||
l.router.With(issuerInterceptor.Handler).Post("/username", l.checkLoginHandler)
|
l.router.Path("/username").Methods("POST").HandlerFunc(issuerInterceptor.HandlerFunc(l.checkLoginHandler))
|
||||||
}
|
}
|
||||||
|
|
||||||
type authenticate interface {
|
type authenticate interface {
|
||||||
|
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/gorilla/mux"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
|
"github.com/zitadel/oidc/v2/example/server/storage"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -47,7 +47,7 @@ func main() {
|
||||||
//be sure to create a proper crypto random key and manage it securely!
|
//be sure to create a proper crypto random key and manage it securely!
|
||||||
key := sha256.Sum256([]byte("test"))
|
key := sha256.Sum256([]byte("test"))
|
||||||
|
|
||||||
router := chi.NewRouter()
|
router := mux.NewRouter()
|
||||||
|
|
||||||
//for simplicity, we provide a very small default page for users who have signed out
|
//for simplicity, we provide a very small default page for users who have signed out
|
||||||
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
|
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
@ -76,7 +76,7 @@ func main() {
|
||||||
|
|
||||||
//regardless of how many pages / steps there are in the process, the UI must be registered in the router,
|
//regardless of how many pages / steps there are in the process, the UI must be registered in the router,
|
||||||
//so we will direct all calls to /login to the login UI
|
//so we will direct all calls to /login to the login UI
|
||||||
router.Mount("/login/", http.StripPrefix("/login", l.router))
|
router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router))
|
||||||
|
|
||||||
//we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
|
//we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
|
||||||
//is served on the correct path
|
//is served on the correct path
|
||||||
|
@ -84,7 +84,7 @@ func main() {
|
||||||
//if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
|
//if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
|
||||||
//then you would have to set the path prefix (/custom/path/):
|
//then you would have to set the path prefix (/custom/path/):
|
||||||
//router.PathPrefix("/custom/path/").Handler(http.StripPrefix("/custom/path", provider.HttpHandler()))
|
//router.PathPrefix("/custom/path/").Handler(http.StripPrefix("/custom/path", provider.HttpHandler()))
|
||||||
router.Mount("/", provider)
|
router.PathPrefix("/").Handler(provider.HttpHandler())
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: ":" + port,
|
Addr: ":" + port,
|
||||||
|
|
|
@ -1,34 +1,21 @@
|
||||||
package exampleop
|
package exampleop
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
"github.com/gorilla/mux"
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/gorilla/securecookie"
|
"github.com/gorilla/securecookie"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
type deviceAuthenticate interface {
|
type deviceAuthenticate interface {
|
||||||
CheckUsernamePasswordSimple(username, password string) error
|
CheckUsernamePasswordSimple(username, password string) error
|
||||||
op.DeviceAuthorizationStorage
|
op.DeviceAuthorizationStorage
|
||||||
|
|
||||||
// GetDeviceAuthorizationByUserCode resturns the current state of the device authorization flow,
|
|
||||||
// identified by the user code.
|
|
||||||
GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error)
|
|
||||||
|
|
||||||
// CompleteDeviceAuthorization marks a device authorization entry as Completed,
|
|
||||||
// identified by userCode. The Subject is added to the state, so that
|
|
||||||
// GetDeviceAuthorizatonState can use it to create a new Access Token.
|
|
||||||
CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error
|
|
||||||
|
|
||||||
// DenyDeviceAuthorization marks a device authorization entry as Denied.
|
|
||||||
DenyDeviceAuthorization(ctx context.Context, userCode string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type deviceLogin struct {
|
type deviceLogin struct {
|
||||||
|
@ -36,14 +23,14 @@ type deviceLogin struct {
|
||||||
cookie *securecookie.SecureCookie
|
cookie *securecookie.SecureCookie
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerDeviceAuth(storage deviceAuthenticate, router chi.Router) {
|
func registerDeviceAuth(storage deviceAuthenticate, router *mux.Router) {
|
||||||
l := &deviceLogin{
|
l := &deviceLogin{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
cookie: securecookie.New(securecookie.GenerateRandomKey(32), nil),
|
cookie: securecookie.New(securecookie.GenerateRandomKey(32), nil),
|
||||||
}
|
}
|
||||||
|
|
||||||
router.HandleFunc("/", l.userCodeHandler)
|
router.HandleFunc("", l.userCodeHandler)
|
||||||
router.Post("/login", l.loginHandler)
|
router.Path("/login").Methods(http.MethodPost).HandlerFunc(l.loginHandler)
|
||||||
router.HandleFunc("/confirm", l.confirmHandler)
|
router.HandleFunc("/confirm", l.confirmHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,29 +5,28 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
"github.com/gorilla/mux"
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type login struct {
|
type login struct {
|
||||||
authenticate authenticate
|
authenticate authenticate
|
||||||
router chi.Router
|
router *mux.Router
|
||||||
callback func(context.Context, string) string
|
callback func(context.Context, string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogin(authenticate authenticate, callback func(context.Context, string) string, issuerInterceptor *op.IssuerInterceptor) *login {
|
func NewLogin(authenticate authenticate, callback func(context.Context, string) string) *login {
|
||||||
l := &login{
|
l := &login{
|
||||||
authenticate: authenticate,
|
authenticate: authenticate,
|
||||||
callback: callback,
|
callback: callback,
|
||||||
}
|
}
|
||||||
l.createRouter(issuerInterceptor)
|
l.createRouter()
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) {
|
func (l *login) createRouter() {
|
||||||
l.router = chi.NewRouter()
|
l.router = mux.NewRouter()
|
||||||
l.router.Get("/username", l.loginHandler)
|
l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler)
|
||||||
l.router.Post("/username", issuerInterceptor.HandlerFunc(l.checkLoginHandler))
|
l.router.Path("/username").Methods("POST").HandlerFunc(l.checkLoginHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
type authenticate interface {
|
type authenticate interface {
|
||||||
|
|
|
@ -3,83 +3,75 @@ package exampleop
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/gorilla/mux"
|
||||||
"github.com/zitadel/logging"
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
"github.com/zitadel/oidc/v2/example/server/storage"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
pathLoggedOut = "/logged-out"
|
pathLoggedOut = "/logged-out"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
storage.RegisterClients(
|
||||||
|
storage.NativeClient("native"),
|
||||||
|
storage.WebClient("web", "secret"),
|
||||||
|
storage.WebClient("api", "secret"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
op.Storage
|
op.Storage
|
||||||
authenticate
|
authenticate
|
||||||
deviceAuthenticate
|
deviceAuthenticate
|
||||||
}
|
}
|
||||||
|
|
||||||
// simple counter for request IDs
|
|
||||||
var counter atomic.Int64
|
|
||||||
|
|
||||||
// SetupServer creates an OIDC server with Issuer=http://localhost:<port>
|
// SetupServer creates an OIDC server with Issuer=http://localhost:<port>
|
||||||
//
|
//
|
||||||
// Use one of the pre-made clients in storage/clients.go or register a new one.
|
// Use one of the pre-made clients in storage/clients.go or register a new one.
|
||||||
func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer bool, extraOptions ...op.Option) chi.Router {
|
func SetupServer(issuer string, storage Storage) *mux.Router {
|
||||||
// the OpenID Provider requires a 32-byte key for (token) encryption
|
// the OpenID Provider requires a 32-byte key for (token) encryption
|
||||||
// be sure to create a proper crypto random key and manage it securely!
|
// be sure to create a proper crypto random key and manage it securely!
|
||||||
key := sha256.Sum256([]byte("test"))
|
key := sha256.Sum256([]byte("test"))
|
||||||
|
|
||||||
router := chi.NewRouter()
|
router := mux.NewRouter()
|
||||||
router.Use(logging.Middleware(
|
|
||||||
logging.WithLogger(logger),
|
|
||||||
logging.WithIDFunc(func() slog.Attr {
|
|
||||||
return slog.Int64("id", counter.Add(1))
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
|
|
||||||
// for simplicity, we provide a very small default page for users who have signed out
|
// for simplicity, we provide a very small default page for users who have signed out
|
||||||
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
|
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
|
||||||
w.Write([]byte("signed out successfully"))
|
_, err := w.Write([]byte("signed out successfully"))
|
||||||
// no need to check/log error, this will be handled by the middleware.
|
if err != nil {
|
||||||
|
log.Printf("error serving logged out page: %v", err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// creation of the OpenIDProvider with the just created in-memory Storage
|
// creation of the OpenIDProvider with the just created in-memory Storage
|
||||||
provider, err := newOP(storage, issuer, key, logger, extraOptions...)
|
provider, err := newOP(storage, issuer, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process
|
// the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process
|
||||||
//for the simplicity of the example this means a simple page with username and password field
|
// for the simplicity of the example this means a simple page with username and password field
|
||||||
//be sure to provide an IssuerInterceptor with the IssuerFromRequest from the OP so the login can select / and pass it to the storage
|
l := NewLogin(storage, op.AuthCallbackURL(provider))
|
||||||
l := NewLogin(storage, op.AuthCallbackURL(provider), op.NewIssuerInterceptor(provider.IssuerFromRequest))
|
|
||||||
|
|
||||||
// regardless of how many pages / steps there are in the process, the UI must be registered in the router,
|
// regardless of how many pages / steps there are in the process, the UI must be registered in the router,
|
||||||
// so we will direct all calls to /login to the login UI
|
// so we will direct all calls to /login to the login UI
|
||||||
router.Mount("/login/", http.StripPrefix("/login", l.router))
|
router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router))
|
||||||
|
|
||||||
router.Route("/device", func(r chi.Router) {
|
router.PathPrefix("/device").Subrouter()
|
||||||
registerDeviceAuth(storage, r)
|
registerDeviceAuth(storage, router.PathPrefix("/device").Subrouter())
|
||||||
})
|
|
||||||
|
|
||||||
handler := http.Handler(provider)
|
|
||||||
if wrapServer {
|
|
||||||
handler = op.RegisterLegacyServer(op.NewLegacyServer(provider, *op.DefaultEndpoints), op.AuthorizeCallbackHandler(provider))
|
|
||||||
}
|
|
||||||
|
|
||||||
// we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
|
// we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
|
||||||
// is served on the correct path
|
// is served on the correct path
|
||||||
//
|
//
|
||||||
// if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
|
// if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
|
||||||
// then you would have to set the path prefix (/custom/path/)
|
// then you would have to set the path prefix (/custom/path/)
|
||||||
router.Mount("/", handler)
|
router.PathPrefix("/").Handler(provider.HttpHandler())
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
@ -87,7 +79,7 @@ func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer
|
||||||
// newOP will create an OpenID Provider for localhost on a specified port with a given encryption key
|
// newOP will create an OpenID Provider for localhost on a specified port with a given encryption key
|
||||||
// and a predefined default logout uri
|
// and a predefined default logout uri
|
||||||
// it will enable all options (see descriptions)
|
// it will enable all options (see descriptions)
|
||||||
func newOP(storage op.Storage, issuer string, key [32]byte, logger *slog.Logger, extraOptions ...op.Option) (op.OpenIDProvider, error) {
|
func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) {
|
||||||
config := &op.Config{
|
config := &op.Config{
|
||||||
CryptoKey: key,
|
CryptoKey: key,
|
||||||
|
|
||||||
|
@ -115,19 +107,15 @@ func newOP(storage op.Storage, issuer string, key [32]byte, logger *slog.Logger,
|
||||||
DeviceAuthorization: op.DeviceAuthorizationConfig{
|
DeviceAuthorization: op.DeviceAuthorizationConfig{
|
||||||
Lifetime: 5 * time.Minute,
|
Lifetime: 5 * time.Minute,
|
||||||
PollInterval: 5 * time.Second,
|
PollInterval: 5 * time.Second,
|
||||||
UserFormPath: "/device",
|
UserFormURL: issuer + "device",
|
||||||
UserCode: op.UserCodeBase20,
|
UserCode: op.UserCodeBase20,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
handler, err := op.NewOpenIDProvider(issuer, config, storage,
|
handler, err := op.NewOpenIDProvider(issuer, config, storage,
|
||||||
append([]op.Option{
|
//we must explicitly allow the use of the http issuer
|
||||||
//we must explicitly allow the use of the http issuer
|
op.WithAllowInsecure(),
|
||||||
op.WithAllowInsecure(),
|
// as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
|
||||||
// as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
|
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
|
||||||
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
|
|
||||||
// Pass our logger to the OP
|
|
||||||
op.WithLogger(logger.WithGroup("op")),
|
|
||||||
}, extraOptions...)...,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -2,58 +2,39 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/config"
|
"github.com/zitadel/oidc/v2/example/server/exampleop"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/exampleop"
|
"github.com/zitadel/oidc/v2/example/server/storage"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getUserStore(cfg *config.Config) (storage.UserStore, error) {
|
|
||||||
if cfg.UsersFile == "" {
|
|
||||||
return storage.NewUserStore(fmt.Sprintf("http://localhost:%s/", cfg.Port)), nil
|
|
||||||
}
|
|
||||||
return storage.StoreFromFile(cfg.UsersFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := config.FromEnvVars(&config.Config{Port: "9998"})
|
//we will run on :9998
|
||||||
logger := slog.New(
|
port := "9998"
|
||||||
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
||||||
AddSource: true,
|
|
||||||
Level: slog.LevelDebug,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
//which gives us the issuer: http://localhost:9998/
|
issuer, ok := os.LookupEnv("ISSUER")
|
||||||
issuer := fmt.Sprintf("http://localhost:%s/", cfg.Port)
|
if !ok {
|
||||||
|
//which gives us the issuer: http://localhost:9998/
|
||||||
storage.RegisterClients(
|
issuer = fmt.Sprintf("http://localhost:%s/", port)
|
||||||
storage.NativeClient("native", cfg.RedirectURI...),
|
}
|
||||||
storage.WebClient("web", "secret", cfg.RedirectURI...),
|
|
||||||
storage.WebClient("api", "secret", cfg.RedirectURI...),
|
|
||||||
)
|
|
||||||
|
|
||||||
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
||||||
// this might be the layer for accessing your database
|
// this might be the layer for accessing your database
|
||||||
// in this example it will be handled in-memory
|
// in this example it will be handled in-memory
|
||||||
store, err := getUserStore(cfg)
|
storage := storage.NewStorage(storage.NewUserStore(issuer))
|
||||||
if err != nil {
|
|
||||||
logger.Error("cannot create UserStore", "error", err)
|
router := exampleop.SetupServer(issuer, storage)
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
storage := storage.NewStorage(store)
|
|
||||||
router := exampleop.SetupServer(issuer, storage, logger, false)
|
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: ":" + cfg.Port,
|
Addr: ":" + port,
|
||||||
Handler: router,
|
Handler: router,
|
||||||
}
|
}
|
||||||
logger.Info("server listening, press ctrl+c to stop", "addr", issuer)
|
log.Printf("server listening on %s", issuer)
|
||||||
if server.ListenAndServe() != http.ErrServerClosed {
|
log.Println("press ctrl+c to stop")
|
||||||
logger.Error("server terminated", "error", err)
|
err := server.ListenAndServe()
|
||||||
os.Exit(1)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ package storage
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -184,26 +184,8 @@ func WebClient(id, secret string, redirectURIs ...string) *Client {
|
||||||
applicationType: op.ApplicationTypeWeb,
|
applicationType: op.ApplicationTypeWeb,
|
||||||
authMethod: oidc.AuthMethodBasic,
|
authMethod: oidc.AuthMethodBasic,
|
||||||
loginURL: defaultLoginURL,
|
loginURL: defaultLoginURL,
|
||||||
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode, oidc.ResponseTypeIDTokenOnly, oidc.ResponseTypeIDToken},
|
|
||||||
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken, oidc.GrantTypeTokenExchange},
|
|
||||||
accessTokenType: op.AccessTokenTypeBearer,
|
|
||||||
devMode: true,
|
|
||||||
idTokenUserinfoClaimsAssertion: false,
|
|
||||||
clockSkew: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeviceClient creates a device client with Basic authentication.
|
|
||||||
func DeviceClient(id, secret string) *Client {
|
|
||||||
return &Client{
|
|
||||||
id: id,
|
|
||||||
secret: secret,
|
|
||||||
redirectURIs: nil,
|
|
||||||
applicationType: op.ApplicationTypeWeb,
|
|
||||||
authMethod: oidc.AuthMethodBasic,
|
|
||||||
loginURL: defaultLoginURL,
|
|
||||||
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
|
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
|
||||||
grantTypes: []oidc.GrantType{oidc.GrantTypeDeviceCode},
|
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
|
||||||
accessTokenType: op.AccessTokenTypeBearer,
|
accessTokenType: op.AccessTokenTypeBearer,
|
||||||
devMode: false,
|
devMode: false,
|
||||||
idTokenUserinfoClaimsAssertion: false,
|
idTokenUserinfoClaimsAssertion: false,
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -35,7 +34,6 @@ type AuthRequest struct {
|
||||||
UserID string
|
UserID string
|
||||||
Scopes []string
|
Scopes []string
|
||||||
ResponseType oidc.ResponseType
|
ResponseType oidc.ResponseType
|
||||||
ResponseMode oidc.ResponseMode
|
|
||||||
Nonce string
|
Nonce string
|
||||||
CodeChallenge *OIDCCodeChallenge
|
CodeChallenge *OIDCCodeChallenge
|
||||||
|
|
||||||
|
@ -43,19 +41,6 @@ type AuthRequest struct {
|
||||||
authTime time.Time
|
authTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogValue allows you to define which fields will be logged.
|
|
||||||
// Implements the [slog.LogValuer]
|
|
||||||
func (a *AuthRequest) LogValue() slog.Value {
|
|
||||||
return slog.GroupValue(
|
|
||||||
slog.String("id", a.ID),
|
|
||||||
slog.Time("creation_date", a.CreationDate),
|
|
||||||
slog.Any("scopes", a.Scopes),
|
|
||||||
slog.String("response_type", string(a.ResponseType)),
|
|
||||||
slog.String("app_id", a.ApplicationID),
|
|
||||||
slog.String("callback_uri", a.CallbackURI),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AuthRequest) GetID() string {
|
func (a *AuthRequest) GetID() string {
|
||||||
return a.ID
|
return a.ID
|
||||||
}
|
}
|
||||||
|
@ -101,7 +86,7 @@ func (a *AuthRequest) GetResponseType() oidc.ResponseType {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
|
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
|
||||||
return a.ResponseMode
|
return "" // we won't handle response mode in this example
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AuthRequest) GetScopes() []string {
|
func (a *AuthRequest) GetScopes() []string {
|
||||||
|
@ -121,7 +106,7 @@ func (a *AuthRequest) Done() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
|
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
|
||||||
prompts := make([]string, 0, len(oidcPrompt))
|
prompts := make([]string, len(oidcPrompt))
|
||||||
for _, oidcPrompt := range oidcPrompt {
|
for _, oidcPrompt := range oidcPrompt {
|
||||||
switch oidcPrompt {
|
switch oidcPrompt {
|
||||||
case oidc.PromptNone,
|
case oidc.PromptNone,
|
||||||
|
@ -155,7 +140,6 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Scopes: authReq.Scopes,
|
Scopes: authReq.Scopes,
|
||||||
ResponseType: authReq.ResponseType,
|
ResponseType: authReq.ResponseType,
|
||||||
ResponseMode: authReq.ResponseMode,
|
|
||||||
Nonce: authReq.Nonce,
|
Nonce: authReq.Nonce,
|
||||||
CodeChallenge: &OIDCCodeChallenge{
|
CodeChallenge: &OIDCCodeChallenge{
|
||||||
Challenge: authReq.CodeChallenge,
|
Challenge: authReq.CodeChallenge,
|
||||||
|
@ -164,15 +148,6 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthRequestWithSessionState struct {
|
|
||||||
*AuthRequest
|
|
||||||
SessionState string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AuthRequestWithSessionState) GetSessionState() string {
|
|
||||||
return a.SessionState
|
|
||||||
}
|
|
||||||
|
|
||||||
type OIDCCodeChallenge struct {
|
type OIDCCodeChallenge struct {
|
||||||
Challenge string
|
Challenge string
|
||||||
Method string
|
Method string
|
||||||
|
|
|
@ -11,11 +11,11 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
|
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
|
||||||
|
@ -28,10 +28,8 @@ var serviceKey1 = &rsa.PublicKey{
|
||||||
E: 65537,
|
E: 65537,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var _ op.Storage = &Storage{}
|
||||||
_ op.Storage = &Storage{}
|
var _ op.ClientCredentialsStorage = &Storage{}
|
||||||
_ op.ClientCredentialsStorage = &Storage{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// storage implements the op.Storage interface
|
// storage implements the op.Storage interface
|
||||||
// typically you would implement this as a layer on top of your database
|
// typically you would implement this as a layer on top of your database
|
||||||
|
@ -61,7 +59,7 @@ func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm {
|
||||||
return s.algorithm
|
return s.algorithm
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *signingKey) Key() any {
|
func (s *signingKey) Key() interface{} {
|
||||||
return s.key
|
return s.key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,15 +83,11 @@ func (s *publicKey) Use() string {
|
||||||
return "sig"
|
return "sig"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *publicKey) Key() any {
|
func (s *publicKey) Key() interface{} {
|
||||||
return &s.key.PublicKey
|
return &s.key.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStorage(userStore UserStore) *Storage {
|
func NewStorage(userStore UserStore) *Storage {
|
||||||
return NewStorageWithClients(userStore, clients)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStorageWithClients(userStore UserStore, clients map[string]*Client) *Storage {
|
|
||||||
key, _ := rsa.GenerateKey(rand.Reader, 2048)
|
key, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
return &Storage{
|
return &Storage{
|
||||||
authRequests: make(map[string]*AuthRequest),
|
authRequests: make(map[string]*AuthRequest),
|
||||||
|
@ -151,9 +145,6 @@ func (s *Storage) CheckUsernamePassword(username, password, id string) error {
|
||||||
// in this example we'll simply check the username / password and set a boolean to true
|
// in this example we'll simply check the username / password and set a boolean to true
|
||||||
// therefore we will also just check this boolean if the request / login has been finished
|
// therefore we will also just check this boolean if the request / login has been finished
|
||||||
request.done = true
|
request.done = true
|
||||||
|
|
||||||
request.authTime = time.Now()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("username or password wrong")
|
return fmt.Errorf("username or password wrong")
|
||||||
|
@ -176,12 +167,6 @@ func (s *Storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthReque
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
if len(authReq.Prompt) == 1 && authReq.Prompt[0] == "none" {
|
|
||||||
// With prompt=none, there is no way for the user to log in
|
|
||||||
// so return error right away.
|
|
||||||
return nil, oidc.ErrLoginRequired()
|
|
||||||
}
|
|
||||||
|
|
||||||
// typically, you'll fill your storage / storage model with the information of the passed object
|
// typically, you'll fill your storage / storage model with the information of the passed object
|
||||||
request := authRequestToInternal(authReq, userID)
|
request := authRequestToInternal(authReq, userID)
|
||||||
|
|
||||||
|
@ -298,19 +283,15 @@ func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.T
|
||||||
|
|
||||||
// if we get here, the currentRefreshToken was not empty, so the call is a refresh token request
|
// if we get here, the currentRefreshToken was not empty, so the call is a refresh token request
|
||||||
// we therefore will have to check the currentRefreshToken and renew the refresh token
|
// we therefore will have to check the currentRefreshToken and renew the refresh token
|
||||||
|
refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken)
|
||||||
newRefreshToken = uuid.NewString()
|
|
||||||
|
|
||||||
accessToken, err := s.accessToken(applicationID, newRefreshToken, request.GetSubject(), request.GetAudience(), request.GetScopes())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", time.Time{}, err
|
return "", "", time.Time{}, err
|
||||||
}
|
}
|
||||||
|
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
|
||||||
if err := s.renewRefreshToken(currentRefreshToken, newRefreshToken, accessToken.ID); err != nil {
|
if err != nil {
|
||||||
return "", "", time.Time{}, err
|
return "", "", time.Time{}, err
|
||||||
}
|
}
|
||||||
|
return accessToken.ID, refreshToken, accessToken.Expiration, nil
|
||||||
return accessToken.ID, newRefreshToken, accessToken.Expiration, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
|
func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
|
||||||
|
@ -392,9 +373,14 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID
|
||||||
if refreshToken.ApplicationID != clientID {
|
if refreshToken.ApplicationID != clientID {
|
||||||
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
|
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
|
||||||
}
|
}
|
||||||
delete(s.refreshTokens, refreshToken.ID)
|
|
||||||
// if it is a refresh token, you will have to remove the access token as well
|
// if it is a refresh token, you will have to remove the access token as well
|
||||||
delete(s.tokens, refreshToken.AccessToken)
|
delete(s.refreshTokens, refreshToken.ID)
|
||||||
|
for _, accessToken := range s.tokens {
|
||||||
|
if accessToken.RefreshTokenID == refreshToken.ID {
|
||||||
|
delete(s.tokens, accessToken.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,9 +476,6 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserI
|
||||||
// return err
|
// return err
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
if token.Expiration.Before(time.Now()) {
|
|
||||||
return fmt.Errorf("token is expired")
|
|
||||||
}
|
|
||||||
return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes)
|
return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -534,11 +517,11 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *
|
||||||
|
|
||||||
// GetPrivateClaimsFromScopes implements the op.Storage interface
|
// GetPrivateClaimsFromScopes implements the op.Storage interface
|
||||||
// it will be called for the creation of a JWT access token to assert claims for custom scopes
|
// it will be called for the creation of a JWT access token to assert claims for custom scopes
|
||||||
func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]any, err error) {
|
func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
|
||||||
return s.getPrivateClaimsFromScopes(ctx, userID, clientID, scopes)
|
return s.getPrivateClaimsFromScopes(ctx, userID, clientID, scopes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]any, err error) {
|
func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
switch scope {
|
switch scope {
|
||||||
case CustomScope:
|
case CustomScope:
|
||||||
|
@ -599,41 +582,33 @@ func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime
|
||||||
Audience: accessToken.Audience,
|
Audience: accessToken.Audience,
|
||||||
Expiration: time.Now().Add(5 * time.Hour),
|
Expiration: time.Now().Add(5 * time.Hour),
|
||||||
Scopes: accessToken.Scopes,
|
Scopes: accessToken.Scopes,
|
||||||
AccessToken: accessToken.ID,
|
|
||||||
}
|
}
|
||||||
s.refreshTokens[token.ID] = token
|
s.refreshTokens[token.ID] = token
|
||||||
return token.Token, nil
|
return token.Token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// renewRefreshToken checks the provided refresh_token and creates a new one based on the current
|
// renewRefreshToken checks the provided refresh_token and creates a new one based on the current
|
||||||
//
|
func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) {
|
||||||
// [Refresh Token Rotation] is implemented.
|
|
||||||
//
|
|
||||||
// [Refresh Token Rotation]: https://www.rfc-editor.org/rfc/rfc6819#section-5.2.2.3
|
|
||||||
func (s *Storage) renewRefreshToken(currentRefreshToken, newRefreshToken, newAccessToken string) error {
|
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
refreshToken, ok := s.refreshTokens[currentRefreshToken]
|
refreshToken, ok := s.refreshTokens[currentRefreshToken]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid refresh token")
|
return "", "", fmt.Errorf("invalid refresh token")
|
||||||
}
|
}
|
||||||
// deletes the refresh token
|
// deletes the refresh token and all access tokens which were issued based on this refresh token
|
||||||
delete(s.refreshTokens, currentRefreshToken)
|
delete(s.refreshTokens, currentRefreshToken)
|
||||||
|
for _, token := range s.tokens {
|
||||||
// delete the access token which was issued based on this refresh token
|
if token.RefreshTokenID == currentRefreshToken {
|
||||||
delete(s.tokens, refreshToken.AccessToken)
|
delete(s.tokens, token.ID)
|
||||||
|
break
|
||||||
if refreshToken.Expiration.Before(time.Now()) {
|
}
|
||||||
return fmt.Errorf("expired refresh token")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// creates a new refresh token based on the current one
|
// creates a new refresh token based on the current one
|
||||||
refreshToken.Token = newRefreshToken
|
token := uuid.NewString()
|
||||||
refreshToken.ID = newRefreshToken
|
refreshToken.Token = token
|
||||||
refreshToken.Expiration = time.Now().Add(5 * time.Hour)
|
refreshToken.ID = token
|
||||||
refreshToken.AccessToken = newAccessToken
|
s.refreshTokens[token] = refreshToken
|
||||||
s.refreshTokens[newRefreshToken] = refreshToken
|
return token, refreshToken.ID, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// accessToken will store an access_token in-memory based on the provided information
|
// accessToken will store an access_token in-memory based on the provided information
|
||||||
|
@ -730,7 +705,7 @@ func (s *Storage) CreateTokenExchangeRequest(ctx context.Context, request op.Tok
|
||||||
// GetPrivateClaimsFromScopesForTokenExchange implements the op.TokenExchangeStorage interface
|
// GetPrivateClaimsFromScopesForTokenExchange implements the op.TokenExchangeStorage interface
|
||||||
// it will be called for the creation of an exchanged JWT access token to assert claims for custom scopes
|
// it will be called for the creation of an exchanged JWT access token to assert claims for custom scopes
|
||||||
// plus adding token exchange specific claims related to delegation or impersonation
|
// plus adding token exchange specific claims related to delegation or impersonation
|
||||||
func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]any, err error) {
|
func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}, err error) {
|
||||||
claims, err = s.getPrivateClaimsFromScopes(ctx, "", request.GetClientID(), request.GetScopes())
|
claims, err = s.getPrivateClaimsFromScopes(ctx, "", request.GetClientID(), request.GetScopes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -759,12 +734,12 @@ func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, useri
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]any) {
|
func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}) {
|
||||||
for _, scope := range request.GetScopes() {
|
for _, scope := range request.GetScopes() {
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(scope, CustomScopeImpersonatePrefix) && request.GetExchangeActor() == "":
|
case strings.HasPrefix(scope, CustomScopeImpersonatePrefix) && request.GetExchangeActor() == "":
|
||||||
// Set actor subject claim for impersonation flow
|
// Set actor subject claim for impersonation flow
|
||||||
claims = appendClaim(claims, "act", map[string]any{
|
claims = appendClaim(claims, "act", map[string]interface{}{
|
||||||
"sub": request.GetExchangeSubject(),
|
"sub": request.GetExchangeSubject(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -772,7 +747,7 @@ func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenEx
|
||||||
|
|
||||||
// Set actor subject claim for delegation flow
|
// Set actor subject claim for delegation flow
|
||||||
// if request.GetExchangeActor() != "" {
|
// if request.GetExchangeActor() != "" {
|
||||||
// claims = appendClaim(claims, "act", map[string]any{
|
// claims = appendClaim(claims, "act", map[string]interface{}{
|
||||||
// "sub": request.GetExchangeActor(),
|
// "sub": request.GetExchangeActor(),
|
||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
|
@ -794,16 +769,16 @@ func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Tim
|
||||||
}
|
}
|
||||||
|
|
||||||
// customClaim demonstrates how to return custom claims based on provided information
|
// customClaim demonstrates how to return custom claims based on provided information
|
||||||
func customClaim(clientID string) map[string]any {
|
func customClaim(clientID string) map[string]interface{} {
|
||||||
return map[string]any{
|
return map[string]interface{}{
|
||||||
"client": clientID,
|
"client": clientID,
|
||||||
"other": "stuff",
|
"other": "stuff",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendClaim(claims map[string]any, claim string, value any) map[string]any {
|
func appendClaim(claims map[string]interface{}, claim string, value interface{}) map[string]interface{} {
|
||||||
if claims == nil {
|
if claims == nil {
|
||||||
claims = make(map[string]any)
|
claims = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
claims[claim] = value
|
claims[claim] = value
|
||||||
return claims
|
return claims
|
||||||
|
|
|
@ -4,10 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
type multiStorage struct {
|
type multiStorage struct {
|
||||||
|
@ -239,7 +239,7 @@ func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspect
|
||||||
|
|
||||||
// GetPrivateClaimsFromScopes implements the op.Storage interface
|
// GetPrivateClaimsFromScopes implements the op.Storage interface
|
||||||
// it will be called for the creation of a JWT access token to assert claims for custom scopes
|
// it will be called for the creation of a JWT access token to assert claims for custom scopes
|
||||||
func (s *multiStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]any, err error) {
|
func (s *multiStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
|
||||||
storage, err := s.storageFromContext(ctx)
|
storage, err := s.storageFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -22,5 +22,4 @@ type RefreshToken struct {
|
||||||
ApplicationID string
|
ApplicationID string
|
||||||
Expiration time.Time
|
Expiration time.Time
|
||||||
Scopes []string
|
Scopes []string
|
||||||
AccessToken string // Token.ID
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
@ -37,18 +35,6 @@ type userStore struct {
|
||||||
users map[string]*User
|
users map[string]*User
|
||||||
}
|
}
|
||||||
|
|
||||||
func StoreFromFile(path string) (UserStore, error) {
|
|
||||||
users := map[string]*User{}
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &users); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return userStore{users}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserStore(issuer string) UserStore {
|
func NewUserStore(issuer string) UserStore {
|
||||||
hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
|
hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
|
||||||
return userStore{
|
return userStore{
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStoreFromFile(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
pathToFile string
|
|
||||||
content string
|
|
||||||
want UserStore
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "normal user file",
|
|
||||||
pathToFile: "userfile.json",
|
|
||||||
content: `{
|
|
||||||
"id1": {
|
|
||||||
"ID": "id1",
|
|
||||||
"EmailVerified": true,
|
|
||||||
"PreferredLanguage": "DE"
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
want: userStore{map[string]*User{
|
|
||||||
"id1": {
|
|
||||||
ID: "id1",
|
|
||||||
EmailVerified: true,
|
|
||||||
PreferredLanguage: language.German,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "malformed file",
|
|
||||||
pathToFile: "whatever",
|
|
||||||
content: "not a json just a text",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "not existing file",
|
|
||||||
pathToFile: "what/ever/file",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
actualPath := path.Join(t.TempDir(), tc.pathToFile)
|
|
||||||
|
|
||||||
if tc.content != "" && tc.pathToFile != "" {
|
|
||||||
if err := os.WriteFile(actualPath, []byte(tc.content), 0666); err != nil {
|
|
||||||
t.Fatalf("cannot create file with test content: %q", tc.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result, err := StoreFromFile(actualPath)
|
|
||||||
if err != nil && !tc.wantErr {
|
|
||||||
t.Errorf("StoreFromFile(%q) returned unexpected error %q", tc.pathToFile, err)
|
|
||||||
} else if err == nil && tc.wantErr {
|
|
||||||
t.Errorf("StoreFromFile(%q) did not return an expected error", tc.pathToFile)
|
|
||||||
}
|
|
||||||
if !tc.wantErr && !reflect.DeepEqual(tc.want, result.(userStore)) {
|
|
||||||
t.Errorf("expected StoreFromFile(%q) = %v, but got %v",
|
|
||||||
tc.pathToFile, tc.want, result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
47
go.mod
47
go.mod
|
@ -1,40 +1,35 @@
|
||||||
module git.christmann.info/LARA/zitadel-oidc/v3
|
module github.com/zitadel/oidc/v2
|
||||||
|
|
||||||
go 1.23.7
|
go 1.18
|
||||||
|
|
||||||
toolchain go1.24.1
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
|
||||||
github.com/go-chi/chi/v5 v5.2.1
|
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5
|
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/go-github/v31 v31.0.0
|
github.com/google/go-github/v31 v31.0.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/gorilla/securecookie v1.1.2
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/jeremija/gosubmit v0.2.8
|
github.com/gorilla/schema v1.2.0
|
||||||
|
github.com/gorilla/securecookie v1.1.1
|
||||||
|
github.com/jeremija/gosubmit v0.2.7
|
||||||
github.com/muhlemmer/gu v0.3.1
|
github.com/muhlemmer/gu v0.3.1
|
||||||
github.com/muhlemmer/httpforwarded v0.1.0
|
github.com/rs/cors v1.8.3
|
||||||
github.com/rs/cors v1.11.1
|
github.com/sirupsen/logrus v1.9.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/stretchr/testify v1.8.2
|
||||||
github.com/stretchr/testify v1.10.0
|
golang.org/x/oauth2 v0.6.0
|
||||||
github.com/zitadel/logging v0.6.2
|
golang.org/x/text v0.8.0
|
||||||
github.com/zitadel/schema v1.3.1
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
go.opentelemetry.io/otel v1.29.0
|
|
||||||
golang.org/x/oauth2 v0.30.0
|
|
||||||
golang.org/x/text v0.26.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
golang.org/x/crypto v0.7.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
golang.org/x/net v0.8.0 // indirect
|
||||||
golang.org/x/crypto v0.36.0 // indirect
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
golang.org/x/net v0.38.0 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
google.golang.org/protobuf v1.29.1 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
99
go.sum
99
go.sum
|
@ -1,80 +1,70 @@
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
|
||||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
|
||||||
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 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
|
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
|
||||||
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
|
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
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/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||||
github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA=
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
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 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
|
github.com/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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
|
||||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU=
|
|
||||||
github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4=
|
|
||||||
github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU=
|
|
||||||
github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU=
|
|
||||||
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
|
||||||
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
|
||||||
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
|
||||||
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
|
||||||
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
|
||||||
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -83,13 +73,14 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
@ -98,11 +89,17 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
|
||||||
|
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var custom = map[string]any{
|
var custom = map[string]any{
|
||||||
|
|
|
@ -8,9 +8,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"gopkg.in/square/go-jose.v2"
|
||||||
"github.com/muhlemmer/gu"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeySet implements oidc.Keys
|
// KeySet implements oidc.Keys
|
||||||
|
@ -18,7 +17,7 @@ type KeySet struct{}
|
||||||
|
|
||||||
// VerifySignature implments op.KeySet.
|
// VerifySignature implments op.KeySet.
|
||||||
func (KeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
|
func (KeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
|
||||||
if err = ctx.Err(); err != nil {
|
if ctx.Err() != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,16 +45,6 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
func signEncodeTokenClaims(claims any) string {
|
||||||
payload, err := json.Marshal(claims)
|
payload, err := json.Marshal(claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -117,25 +106,6 @@ func NewAccessToken(issuer, subject string, audience []string, expiration time.T
|
||||||
return NewAccessTokenCustom(issuer, subject, audience, expiration, jwtid, clientID, skew, nil)
|
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`
|
const InvalidSignatureToken = `eyJhbGciOiJQUzUxMiJ9.eyJpc3MiOiJsb2NhbC5jb20iLCJzdWIiOiJ0aW1AbG9jYWwuY29tIiwiYXVkIjpbInVuaXQiLCJ0ZXN0IiwiNTU1NjY2Il0sImV4cCI6MTY3Nzg0MDQzMSwiaWF0IjoxNjc3ODQwMzcwLCJhdXRoX3RpbWUiOjE2Nzc4NDAzMTAsIm5vbmNlIjoiMTIzNDUiLCJhY3IiOiJzb21ldGhpbmciLCJhbXIiOlsiZm9vIiwiYmFyIl0sImF6cCI6IjU1NTY2NiJ9.DtZmvVkuE4Hw48ijBMhRJbxEWCr_WEYuPQBMY73J9TP6MmfeNFkjVJf4nh4omjB9gVLnQ-xhEkNOe62FS5P0BB2VOxPuHZUj34dNspCgG3h98fGxyiMb5vlIYAHDF9T-w_LntlYItohv63MmdYR-hPpAqjXE7KOfErf-wUDGE9R3bfiQ4HpTdyFJB1nsToYrZ9lhP2mzjTCTs58ckZfQ28DFHn_lfHWpR4rJBgvLx7IH4rMrUayr09Ap-PxQLbv0lYMtmgG1z3JK8MXnuYR0UJdZnEIezOzUTlThhCXB-nvuAXYjYxZZTR0FtlgZUHhIpYK0V2abf_Q_Or36akNCUg`
|
||||||
|
|
||||||
// These variables always result in a valid token
|
// These variables always result in a valid token
|
||||||
|
@ -167,10 +137,6 @@ func ValidAccessToken() (string, *oidc.AccessTokenClaims) {
|
||||||
return NewAccessToken(ValidIssuer, ValidSubject, ValidAudience, ValidExpiration, ValidJWTID, ValidClientID, ValidSkew)
|
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.
|
// ACRVerify is a oidc.ACRVerifier func.
|
||||||
func ACRVerify(acr string) error {
|
func ACRVerify(acr string) error {
|
||||||
if acr != ValidACR {
|
if acr != ValidACR {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -10,44 +11,32 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-jose/go-jose/v4"
|
|
||||||
"github.com/zitadel/logging"
|
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
|
"github.com/zitadel/oidc/v2/pkg/crypto"
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var Encoder = httphelper.Encoder(oidc.NewEncoder())
|
||||||
Encoder = httphelper.Encoder(oidc.NewEncoder())
|
|
||||||
Tracer = otel.Tracer("github.com/zitadel/oidc/pkg/client")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Discover calls the discovery endpoint of the provided issuer and returns its configuration
|
// Discover calls the discovery endpoint of the provided issuer and returns its configuration
|
||||||
// It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url
|
// It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url
|
||||||
func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) {
|
func Discover(issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) {
|
||||||
ctx, span := Tracer.Start(ctx, "Discover")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
|
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
|
||||||
if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" {
|
if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" {
|
||||||
wellKnown = wellKnownUrl[0]
|
wellKnown = wellKnownUrl[0]
|
||||||
}
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, wellKnown, nil)
|
req, err := http.NewRequest("GET", wellKnown, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
discoveryConfig := new(oidc.DiscoveryConfiguration)
|
discoveryConfig := new(oidc.DiscoveryConfiguration)
|
||||||
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
|
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Join(oidc.ErrDiscoveryFailed, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
if logger, ok := logging.FromContext(ctx); ok {
|
|
||||||
logger.Debug("discover", "config", discoveryConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
if discoveryConfig.Issuer != issuer {
|
if discoveryConfig.Issuer != issuer {
|
||||||
return nil, oidc.ErrIssuerInvalid
|
return nil, oidc.ErrIssuerInvalid
|
||||||
}
|
}
|
||||||
|
@ -59,15 +48,12 @@ type TokenEndpointCaller interface {
|
||||||
HttpClient() *http.Client
|
HttpClient() *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallTokenEndpoint(ctx context.Context, request any, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) {
|
func CallTokenEndpoint(request interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) {
|
||||||
return callTokenEndpoint(ctx, request, nil, caller)
|
return callTokenEndpoint(request, nil, caller)
|
||||||
}
|
}
|
||||||
|
|
||||||
func callTokenEndpoint(ctx context.Context, request any, authFn any, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) {
|
func callTokenEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) {
|
||||||
ctx, span := Tracer.Start(ctx, "callTokenEndpoint")
|
req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn)
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -75,18 +61,12 @@ func callTokenEndpoint(ctx context.Context, request any, authFn any, caller Toke
|
||||||
if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil {
|
if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
token := &oauth2.Token{
|
return &oauth2.Token{
|
||||||
AccessToken: tokenRes.AccessToken,
|
AccessToken: tokenRes.AccessToken,
|
||||||
TokenType: tokenRes.TokenType,
|
TokenType: tokenRes.TokenType,
|
||||||
RefreshToken: tokenRes.RefreshToken,
|
RefreshToken: tokenRes.RefreshToken,
|
||||||
Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second),
|
Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second),
|
||||||
}
|
}, nil
|
||||||
if tokenRes.IDToken != "" {
|
|
||||||
token = token.WithExtra(map[string]any{
|
|
||||||
"id_token": tokenRes.IDToken,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return token, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EndSessionCaller interface {
|
type EndSessionCaller interface {
|
||||||
|
@ -94,16 +74,8 @@ type EndSessionCaller interface {
|
||||||
HttpClient() *http.Client
|
HttpClient() *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallEndSessionEndpoint(ctx context.Context, request any, authFn any, caller EndSessionCaller) (*url.URL, error) {
|
func CallEndSessionEndpoint(request interface{}, authFn interface{}, caller EndSessionCaller) (*url.URL, error) {
|
||||||
ctx, span := Tracer.Start(ctx, "CallEndSessionEndpoint")
|
req, err := httphelper.FormRequest(caller.GetEndSessionEndpoint(), request, Encoder, authFn)
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
endpoint := caller.GetEndSessionEndpoint()
|
|
||||||
if endpoint == "" {
|
|
||||||
return nil, fmt.Errorf("end session %w", ErrEndpointNotSet)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -145,16 +117,8 @@ type RevokeRequest struct {
|
||||||
ClientSecret string `schema:"client_secret"`
|
ClientSecret string `schema:"client_secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller RevokeCaller) error {
|
func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCaller) error {
|
||||||
ctx, span := Tracer.Start(ctx, "CallRevokeEndpoint")
|
req, err := httphelper.FormRequest(caller.GetRevokeEndpoint(), request, Encoder, authFn)
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
endpoint := caller.GetRevokeEndpoint()
|
|
||||||
if endpoint == "" {
|
|
||||||
return fmt.Errorf("revoke %w", ErrEndpointNotSet)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -181,11 +145,8 @@ func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller Rev
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) {
|
func CallTokenExchangeEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) {
|
||||||
ctx, span := Tracer.Start(ctx, "CallTokenExchangeEndpoint")
|
req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn)
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -197,12 +158,12 @@ func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, cal
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
|
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
|
||||||
privateKey, algorithm, err := crypto.BytesToPrivateKey(key)
|
privateKey, err := crypto.BytesToPrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
signingKey := jose.SigningKey{
|
signingKey := jose.SigningKey{
|
||||||
Algorithm: algorithm,
|
Algorithm: jose.RS256,
|
||||||
Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID},
|
Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID},
|
||||||
}
|
}
|
||||||
return jose.NewSigner(signingKey, &jose.SignerOptions{})
|
return jose.NewSigner(signingKey, &jose.SignerOptions{})
|
||||||
|
@ -225,16 +186,8 @@ type DeviceAuthorizationCaller interface {
|
||||||
HttpClient() *http.Client
|
HttpClient() *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallDeviceAuthorizationEndpoint(ctx context.Context, request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller, authFn any) (*oidc.DeviceAuthorizationResponse, error) {
|
func CallDeviceAuthorizationEndpoint(request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) {
|
||||||
ctx, span := Tracer.Start(ctx, "CallDeviceAuthorizationEndpoint")
|
req, err := httphelper.FormRequest(caller.GetDeviceAuthorizationEndpoint(), request, Encoder, nil)
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
endpoint := caller.GetDeviceAuthorizationEndpoint()
|
|
||||||
if endpoint == "" {
|
|
||||||
return nil, fmt.Errorf("device authorization %w", ErrEndpointNotSet)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -255,10 +208,7 @@ type DeviceAccessTokenRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
|
func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
|
||||||
ctx, span := Tracer.Start(ctx, "CallDeviceAccessTokenEndpoint")
|
req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, nil)
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -266,17 +216,28 @@ func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTok
|
||||||
req.SetBasicAuth(request.ClientID, request.ClientSecret)
|
req.SetBasicAuth(request.ClientID, request.ClientSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := new(oidc.AccessTokenResponse)
|
httpResp, err := caller.HttpClient().Do(req)
|
||||||
if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return resp, nil
|
defer httpResp.Body.Close()
|
||||||
|
|
||||||
|
resp := new(struct {
|
||||||
|
*oidc.AccessTokenResponse
|
||||||
|
*oidc.Error
|
||||||
|
})
|
||||||
|
if err = json.NewDecoder(httpResp.Body).Decode(resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if httpResp.StatusCode == http.StatusOK {
|
||||||
|
return resp.AccessTokenResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, resp.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
|
func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
|
||||||
ctx, span := Tracer.Start(ctx, "PollDeviceAccessTokenEndpoint")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
timer := time.After(interval)
|
timer := time.After(interval)
|
||||||
select {
|
select {
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDiscover(t *testing.T) {
|
|
||||||
type wantFields struct {
|
|
||||||
UILocalesSupported bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type args struct {
|
|
||||||
issuer string
|
|
||||||
wellKnownUrl []string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantFields *wantFields
|
|
||||||
wantErr error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "spotify", // https://github.com/zitadel/oidc/issues/406
|
|
||||||
args: args{
|
|
||||||
issuer: "https://accounts.spotify.com",
|
|
||||||
},
|
|
||||||
wantFields: &wantFields{
|
|
||||||
UILocalesSupported: true,
|
|
||||||
},
|
|
||||||
wantErr: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "discovery failed",
|
|
||||||
args: args{
|
|
||||||
issuer: "https://example.com",
|
|
||||||
},
|
|
||||||
wantErr: oidc.ErrDiscoveryFailed,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := Discover(context.Background(), tt.args.issuer, http.DefaultClient, tt.args.wellKnownUrl...)
|
|
||||||
require.ErrorIs(t, err, tt.wantErr)
|
|
||||||
if tt.wantFields == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.Equal(t, tt.args.issuer, got.Issuer)
|
|
||||||
if tt.wantFields.UILocalesSupported {
|
|
||||||
assert.NotEmpty(t, got.UILocalesSupported)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var ErrEndpointNotSet = errors.New("endpoint not set")
|
|
|
@ -2,65 +2,32 @@ package client_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"io/ioutil"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jeremija/gosubmit"
|
"github.com/jeremija/gosubmit"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/oauth2"
|
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/exampleop"
|
"github.com/zitadel/oidc/v2/example/server/exampleop"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
|
"github.com/zitadel/oidc/v2/example/server/storage"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs"
|
"github.com/zitadel/oidc/v2/pkg/client/rs"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/tokenexchange"
|
"github.com/zitadel/oidc/v2/pkg/client/tokenexchange"
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Logger = slog.New(
|
|
||||||
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
||||||
AddSource: true,
|
|
||||||
Level: slog.LevelDebug,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
var CTX context.Context
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
os.Exit(func() int {
|
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT)
|
|
||||||
defer cancel()
|
|
||||||
CTX, cancel = context.WithTimeout(ctx, time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
return m.Run()
|
|
||||||
}())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRelyingPartySession(t *testing.T) {
|
func TestRelyingPartySession(t *testing.T) {
|
||||||
for _, wrapServer := range []bool{false, true} {
|
|
||||||
t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) {
|
|
||||||
testRelyingPartySession(t, wrapServer)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRelyingPartySession(t *testing.T, wrapServer bool) {
|
|
||||||
t.Log("------- start example OP ------")
|
t.Log("------- start example OP ------")
|
||||||
targetURL := "http://local-site"
|
targetURL := "http://local-site"
|
||||||
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
|
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
|
||||||
|
@ -68,17 +35,17 @@ func testRelyingPartySession(t *testing.T, wrapServer bool) {
|
||||||
opServer := httptest.NewServer(&dh)
|
opServer := httptest.NewServer(&dh)
|
||||||
defer opServer.Close()
|
defer opServer.Close()
|
||||||
t.Logf("auth server at %s", opServer.URL)
|
t.Logf("auth server at %s", opServer.URL)
|
||||||
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, wrapServer)
|
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage)
|
||||||
|
|
||||||
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
||||||
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
||||||
|
|
||||||
t.Log("------- run authorization code flow ------")
|
t.Log("------- run authorization code flow ------")
|
||||||
provider, tokens := RunAuthorizationCodeFlow(t, opServer, clientID, "secret")
|
provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, "secret")
|
||||||
|
|
||||||
t.Log("------- refresh tokens ------")
|
t.Log("------- refresh tokens ------")
|
||||||
|
|
||||||
newTokens, err := rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "")
|
newTokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "")
|
||||||
require.NoError(t, err, "refresh token")
|
require.NoError(t, err, "refresh token")
|
||||||
assert.NotNil(t, newTokens, "access token")
|
assert.NotNil(t, newTokens, "access token")
|
||||||
t.Logf("new access token %s", newTokens.AccessToken)
|
t.Logf("new access token %s", newTokens.AccessToken)
|
||||||
|
@ -86,13 +53,10 @@ func testRelyingPartySession(t *testing.T, wrapServer bool) {
|
||||||
t.Logf("new token type %s", newTokens.TokenType)
|
t.Logf("new token type %s", newTokens.TokenType)
|
||||||
t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339))
|
t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339))
|
||||||
require.NotEmpty(t, newTokens.AccessToken, "new accessToken")
|
require.NotEmpty(t, newTokens.AccessToken, "new accessToken")
|
||||||
assert.NotEmpty(t, newTokens.IDToken, "new idToken")
|
|
||||||
assert.NotNil(t, newTokens.IDTokenClaims)
|
|
||||||
assert.Equal(t, newTokens.IDTokenClaims.Subject, tokens.IDTokenClaims.Subject)
|
|
||||||
|
|
||||||
t.Log("------ end session (logout) ------")
|
t.Log("------ end session (logout) ------")
|
||||||
|
|
||||||
newLoc, err := rp.EndSession(CTX, provider, tokens.IDToken, "", "")
|
newLoc, err := rp.EndSession(provider, idToken, "", "")
|
||||||
require.NoError(t, err, "logout")
|
require.NoError(t, err, "logout")
|
||||||
if newLoc != nil {
|
if newLoc != nil {
|
||||||
t.Logf("redirect to %s", newLoc)
|
t.Logf("redirect to %s", newLoc)
|
||||||
|
@ -101,111 +65,17 @@ func testRelyingPartySession(t *testing.T, wrapServer bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Log("------ attempt refresh again (should fail) ------")
|
t.Log("------ attempt refresh again (should fail) ------")
|
||||||
t.Log("trying original refresh token", tokens.RefreshToken)
|
t.Log("trying original refresh token", refreshToken)
|
||||||
_, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "")
|
_, err = rp.RefreshAccessToken(provider, refreshToken, "", "")
|
||||||
assert.Errorf(t, err, "refresh with original")
|
assert.Errorf(t, err, "refresh with original")
|
||||||
if newTokens.RefreshToken != "" {
|
if newTokens.RefreshToken != "" {
|
||||||
t.Log("trying replacement refresh token", newTokens.RefreshToken)
|
t.Log("trying replacement refresh token", newTokens.RefreshToken)
|
||||||
_, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, newTokens.RefreshToken, "", "")
|
_, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "")
|
||||||
assert.Errorf(t, err, "refresh with replacement")
|
assert.Errorf(t, err, "refresh with replacement")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRelyingPartyWithSigningAlgsFromDiscovery(t *testing.T) {
|
|
||||||
targetURL := "http://local-site"
|
|
||||||
localURL, err := url.Parse(targetURL + "/login?requestID=1234")
|
|
||||||
require.NoError(t, err, "local url")
|
|
||||||
|
|
||||||
t.Log("------- start example OP ------")
|
|
||||||
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
|
||||||
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
|
||||||
clientSecret := "secret"
|
|
||||||
client := storage.WebClient(clientID, clientSecret, targetURL)
|
|
||||||
storage.RegisterClients(client)
|
|
||||||
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
|
|
||||||
var dh deferredHandler
|
|
||||||
opServer := httptest.NewServer(&dh)
|
|
||||||
defer opServer.Close()
|
|
||||||
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, true)
|
|
||||||
|
|
||||||
t.Log("------- create RP ------")
|
|
||||||
provider, err := rp.NewRelyingPartyOIDC(
|
|
||||||
CTX,
|
|
||||||
opServer.URL,
|
|
||||||
clientID,
|
|
||||||
clientSecret,
|
|
||||||
targetURL,
|
|
||||||
[]string{"openid"},
|
|
||||||
rp.WithSigningAlgsFromDiscovery(),
|
|
||||||
)
|
|
||||||
require.NoError(t, err, "new rp")
|
|
||||||
|
|
||||||
t.Log("------- run authorization code flow ------")
|
|
||||||
jar, err := cookiejar.New(nil)
|
|
||||||
require.NoError(t, err, "create cookie jar")
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Timeout: time.Second * 5,
|
|
||||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
|
||||||
return http.ErrUseLastResponse
|
|
||||||
},
|
|
||||||
Jar: jar,
|
|
||||||
}
|
|
||||||
state := "state-" + strconv.FormatInt(seed.Int63(), 25)
|
|
||||||
capturedW := httptest.NewRecorder()
|
|
||||||
get := httptest.NewRequest("GET", localURL.String(), nil)
|
|
||||||
rp.AuthURLHandler(func() string { return state }, provider,
|
|
||||||
rp.WithPromptURLParam("Hello, World!", "Goodbye, World!"),
|
|
||||||
rp.WithURLParam("custom", "param"),
|
|
||||||
)(capturedW, get)
|
|
||||||
defer func() {
|
|
||||||
if t.Failed() {
|
|
||||||
t.Log("response body (redirect from RP to OP)", capturedW.Body.String())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
resp := capturedW.Result()
|
|
||||||
startAuthURL, err := resp.Location()
|
|
||||||
require.NoError(t, err, "get redirect")
|
|
||||||
loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL)
|
|
||||||
form := getForm(t, "get login form", httpClient, loginPageURL)
|
|
||||||
defer func() {
|
|
||||||
if t.Failed() {
|
|
||||||
t.Logf("login form (unfilled): %s", string(form))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL,
|
|
||||||
gosubmit.Set("username", "test-user@local-site"),
|
|
||||||
gosubmit.Set("password", "verysecure"),
|
|
||||||
)
|
|
||||||
codeBearingURL := getRedirect(t, "get redirect with code", httpClient, postLoginRedirectURL)
|
|
||||||
capturedW = httptest.NewRecorder()
|
|
||||||
get = httptest.NewRequest("GET", codeBearingURL.String(), nil)
|
|
||||||
var idToken string
|
|
||||||
redirect := func(w http.ResponseWriter, r *http.Request, newTokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
|
||||||
idToken = newTokens.IDToken
|
|
||||||
http.Redirect(w, r, targetURL, http.StatusFound)
|
|
||||||
}
|
|
||||||
rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get)
|
|
||||||
defer func() {
|
|
||||||
if t.Failed() {
|
|
||||||
t.Log("token exchange response body", capturedW.Body.String())
|
|
||||||
require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
t.Log("------- verify id token ------")
|
|
||||||
_, err = rp.VerifyIDToken[*oidc.IDTokenClaims](CTX, idToken, provider.IDTokenVerifier())
|
|
||||||
require.NoError(t, err, "verify id token")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceServerTokenExchange(t *testing.T) {
|
func TestResourceServerTokenExchange(t *testing.T) {
|
||||||
for _, wrapServer := range []bool{false, true} {
|
|
||||||
t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) {
|
|
||||||
testResourceServerTokenExchange(t, wrapServer)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
|
|
||||||
t.Log("------- start example OP ------")
|
t.Log("------- start example OP ------")
|
||||||
targetURL := "http://local-site"
|
targetURL := "http://local-site"
|
||||||
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
|
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
|
||||||
|
@ -213,24 +83,23 @@ func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
|
||||||
opServer := httptest.NewServer(&dh)
|
opServer := httptest.NewServer(&dh)
|
||||||
defer opServer.Close()
|
defer opServer.Close()
|
||||||
t.Logf("auth server at %s", opServer.URL)
|
t.Logf("auth server at %s", opServer.URL)
|
||||||
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, wrapServer)
|
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage)
|
||||||
|
|
||||||
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
||||||
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
||||||
clientSecret := "secret"
|
clientSecret := "secret"
|
||||||
|
|
||||||
t.Log("------- run authorization code flow ------")
|
t.Log("------- run authorization code flow ------")
|
||||||
provider, tokens := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret)
|
provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret)
|
||||||
|
|
||||||
resourceServer, err := rs.NewResourceServerClientCredentials(CTX, opServer.URL, clientID, clientSecret)
|
resourceServer, err := rs.NewResourceServerClientCredentials(opServer.URL, clientID, clientSecret)
|
||||||
require.NoError(t, err, "new resource server")
|
require.NoError(t, err, "new resource server")
|
||||||
|
|
||||||
t.Log("------- exchage refresh tokens (impersonation) ------")
|
t.Log("------- exchage refresh tokens (impersonation) ------")
|
||||||
|
|
||||||
tokenExchangeResponse, err := tokenexchange.ExchangeToken(
|
tokenExchangeResponse, err := tokenexchange.ExchangeToken(
|
||||||
CTX,
|
|
||||||
resourceServer,
|
resourceServer,
|
||||||
tokens.RefreshToken,
|
refreshToken,
|
||||||
oidc.RefreshTokenType,
|
oidc.RefreshTokenType,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
@ -248,7 +117,7 @@ func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
|
||||||
|
|
||||||
t.Log("------ end session (logout) ------")
|
t.Log("------ end session (logout) ------")
|
||||||
|
|
||||||
newLoc, err := rp.EndSession(CTX, provider, tokens.IDToken, "", "")
|
newLoc, err := rp.EndSession(provider, idToken, "", "")
|
||||||
require.NoError(t, err, "logout")
|
require.NoError(t, err, "logout")
|
||||||
if newLoc != nil {
|
if newLoc != nil {
|
||||||
t.Logf("redirect to %s", newLoc)
|
t.Logf("redirect to %s", newLoc)
|
||||||
|
@ -259,9 +128,8 @@ func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
|
||||||
t.Log("------- attempt exchage again (should fail) ------")
|
t.Log("------- attempt exchage again (should fail) ------")
|
||||||
|
|
||||||
tokenExchangeResponse, err = tokenexchange.ExchangeToken(
|
tokenExchangeResponse, err = tokenexchange.ExchangeToken(
|
||||||
CTX,
|
|
||||||
resourceServer,
|
resourceServer,
|
||||||
tokens.RefreshToken,
|
refreshToken,
|
||||||
oidc.RefreshTokenType,
|
oidc.RefreshTokenType,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
@ -273,9 +141,10 @@ func testResourceServerTokenExchange(t *testing.T, wrapServer bool) {
|
||||||
require.Error(t, err, "refresh token")
|
require.Error(t, err, "refresh token")
|
||||||
assert.Contains(t, err.Error(), "subject_token is invalid")
|
assert.Contains(t, err.Error(), "subject_token is invalid")
|
||||||
require.Nil(t, tokenExchangeResponse, "token exchange response")
|
require.Nil(t, tokenExchangeResponse, "token exchange response")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, clientSecret string) (provider rp.RelyingParty, tokens *oidc.Tokens[*oidc.IDTokenClaims]) {
|
func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, clientSecret string) (provider rp.RelyingParty, accessToken, refreshToken, idToken string) {
|
||||||
targetURL := "http://local-site"
|
targetURL := "http://local-site"
|
||||||
localURL, err := url.Parse(targetURL + "/login?requestID=1234")
|
localURL, err := url.Parse(targetURL + "/login?requestID=1234")
|
||||||
require.NoError(t, err, "local url")
|
require.NoError(t, err, "local url")
|
||||||
|
@ -297,14 +166,12 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
|
||||||
key := []byte("test1234test1234")
|
key := []byte("test1234test1234")
|
||||||
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
||||||
provider, err = rp.NewRelyingPartyOIDC(
|
provider, err = rp.NewRelyingPartyOIDC(
|
||||||
CTX,
|
|
||||||
opServer.URL,
|
opServer.URL,
|
||||||
clientID,
|
clientID,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
targetURL,
|
targetURL,
|
||||||
[]string{"openid", "email", "profile", "offline_access"},
|
[]string{"openid", "email", "profile", "offline_access"},
|
||||||
rp.WithPKCE(cookieHandler),
|
rp.WithPKCE(cookieHandler),
|
||||||
rp.WithAuthStyle(oauth2.AuthStyleInHeader),
|
|
||||||
rp.WithVerifierOpts(
|
rp.WithVerifierOpts(
|
||||||
rp.WithIssuedAtOffset(5*time.Second),
|
rp.WithIssuedAtOffset(5*time.Second),
|
||||||
rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"),
|
rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"),
|
||||||
|
@ -373,8 +240,7 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
|
||||||
}
|
}
|
||||||
|
|
||||||
var email string
|
var email string
|
||||||
redirect := func(w http.ResponseWriter, r *http.Request, newTokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
||||||
tokens = newTokens
|
|
||||||
require.NotNil(t, tokens, "tokens")
|
require.NotNil(t, tokens, "tokens")
|
||||||
require.NotNil(t, info, "info")
|
require.NotNil(t, info, "info")
|
||||||
t.Log("access token", tokens.AccessToken)
|
t.Log("access token", tokens.AccessToken)
|
||||||
|
@ -382,6 +248,9 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
|
||||||
t.Log("id token", tokens.IDToken)
|
t.Log("id token", tokens.IDToken)
|
||||||
t.Log("email", info.Email)
|
t.Log("email", info.Email)
|
||||||
|
|
||||||
|
accessToken = tokens.AccessToken
|
||||||
|
refreshToken = tokens.RefreshToken
|
||||||
|
idToken = tokens.IDToken
|
||||||
email = info.Email
|
email = info.Email
|
||||||
http.Redirect(w, r, targetURL, 302)
|
http.Redirect(w, r, targetURL, 302)
|
||||||
}
|
}
|
||||||
|
@ -403,124 +272,12 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
|
||||||
require.NoError(t, err, "get fully-authorizied redirect location")
|
require.NoError(t, err, "get fully-authorizied redirect location")
|
||||||
require.Equal(t, targetURL, authorizedURL.String(), "fully-authorizied redirect location")
|
require.Equal(t, targetURL, authorizedURL.String(), "fully-authorizied redirect location")
|
||||||
|
|
||||||
require.NotEmpty(t, tokens.IDToken, "id token")
|
require.NotEmpty(t, idToken, "id token")
|
||||||
assert.NotEmpty(t, tokens.RefreshToken, "refresh token")
|
assert.NotEmpty(t, refreshToken, "refresh token")
|
||||||
assert.NotEmpty(t, tokens.AccessToken, "access token")
|
assert.NotEmpty(t, accessToken, "access token")
|
||||||
assert.NotEmpty(t, email, "email")
|
assert.NotEmpty(t, email, "email")
|
||||||
|
|
||||||
return provider, tokens
|
return provider, accessToken, refreshToken, idToken
|
||||||
}
|
|
||||||
|
|
||||||
func TestClientCredentials(t *testing.T) {
|
|
||||||
targetURL := "http://local-site"
|
|
||||||
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
|
|
||||||
var dh deferredHandler
|
|
||||||
opServer := httptest.NewServer(&dh)
|
|
||||||
defer opServer.Close()
|
|
||||||
t.Logf("auth server at %s", opServer.URL)
|
|
||||||
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, true)
|
|
||||||
|
|
||||||
provider, err := rp.NewRelyingPartyOIDC(
|
|
||||||
CTX,
|
|
||||||
opServer.URL,
|
|
||||||
"sid1",
|
|
||||||
"verysecret",
|
|
||||||
targetURL,
|
|
||||||
[]string{"openid"},
|
|
||||||
)
|
|
||||||
require.NoError(t, err, "new rp")
|
|
||||||
|
|
||||||
token, err := rp.ClientCredentials(CTX, provider, nil)
|
|
||||||
require.NoError(t, err, "ClientCredentials call")
|
|
||||||
require.NotNil(t, token)
|
|
||||||
assert.NotEmpty(t, token.AccessToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrorFromPromptNone(t *testing.T) {
|
|
||||||
jar, err := cookiejar.New(nil)
|
|
||||||
require.NoError(t, err, "create cookie jar")
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Timeout: time.Second * 5,
|
|
||||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
|
||||||
return http.ErrUseLastResponse
|
|
||||||
},
|
|
||||||
Jar: jar,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("------- start example OP ------")
|
|
||||||
targetURL := "http://local-site"
|
|
||||||
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
|
|
||||||
var dh deferredHandler
|
|
||||||
opServer := httptest.NewServer(&dh)
|
|
||||||
defer opServer.Close()
|
|
||||||
t.Logf("auth server at %s", opServer.URL)
|
|
||||||
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, false, op.WithHttpInterceptors(
|
|
||||||
func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
t.Logf("request to %s", r.URL)
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
))
|
|
||||||
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
|
||||||
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
|
||||||
clientSecret := "secret"
|
|
||||||
client := storage.WebClient(clientID, clientSecret, targetURL)
|
|
||||||
storage.RegisterClients(client)
|
|
||||||
|
|
||||||
t.Log("------- create RP ------")
|
|
||||||
key := []byte("test1234test1234")
|
|
||||||
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
|
||||||
provider, err := rp.NewRelyingPartyOIDC(
|
|
||||||
CTX,
|
|
||||||
opServer.URL,
|
|
||||||
clientID,
|
|
||||||
clientSecret,
|
|
||||||
targetURL,
|
|
||||||
[]string{"openid", "email", "profile", "offline_access"},
|
|
||||||
rp.WithPKCE(cookieHandler),
|
|
||||||
rp.WithVerifierOpts(
|
|
||||||
rp.WithIssuedAtOffset(5*time.Second),
|
|
||||||
rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
require.NoError(t, err, "new rp")
|
|
||||||
|
|
||||||
t.Log("------- start auth flow with prompt=none ------- ")
|
|
||||||
state := "state-32892"
|
|
||||||
capturedW := httptest.NewRecorder()
|
|
||||||
localURL, err := url.Parse(targetURL + "/login")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
get := httptest.NewRequest("GET", localURL.String(), nil)
|
|
||||||
rp.AuthURLHandler(func() string { return state }, provider,
|
|
||||||
rp.WithPromptURLParam("none"),
|
|
||||||
rp.WithResponseModeURLParam(oidc.ResponseModeFragment),
|
|
||||||
)(capturedW, get)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if t.Failed() {
|
|
||||||
t.Log("response body (redirect from RP to OP)", capturedW.Body.String())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code")
|
|
||||||
require.Less(t, capturedW.Code, 400, "captured response code")
|
|
||||||
|
|
||||||
//nolint:bodyclose
|
|
||||||
resp := capturedW.Result()
|
|
||||||
jar.SetCookies(localURL, resp.Cookies())
|
|
||||||
|
|
||||||
startAuthURL, err := resp.Location()
|
|
||||||
require.NoError(t, err, "get redirect")
|
|
||||||
assert.NotEmpty(t, startAuthURL, "login url")
|
|
||||||
t.Log("Starting auth at", startAuthURL)
|
|
||||||
|
|
||||||
t.Log("------- get redirect from OP ------")
|
|
||||||
loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL)
|
|
||||||
t.Log("login page URL", loginPageURL)
|
|
||||||
|
|
||||||
require.Contains(t, loginPageURL.String(), `error=login_required`, "prompt=none should error")
|
|
||||||
require.Contains(t, loginPageURL.String(), `local-site#error=`, "response_mode=fragment means '#' instead of '?'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type deferredHandler struct {
|
type deferredHandler struct {
|
||||||
|
@ -568,7 +325,7 @@ func getForm(t *testing.T, desc string, httpClient *http.Client, uri *url.URL) [
|
||||||
|
|
||||||
func fillForm(t *testing.T, desc string, httpClient *http.Client, body []byte, uri *url.URL, opts ...gosubmit.Option) *url.URL {
|
func fillForm(t *testing.T, desc string, httpClient *http.Client, body []byte, uri *url.URL, opts ...gosubmit.Option) *url.URL {
|
||||||
// TODO: switch to io.NopCloser when go1.15 support is dropped
|
// TODO: switch to io.NopCloser when go1.15 support is dropped
|
||||||
req := gosubmit.ParseWithURL(io.NopCloser(bytes.NewReader(body)), uri.String()).FirstForm().Testing(t).NewTestRequest(
|
req := gosubmit.ParseWithURL(ioutil.NopCloser(bytes.NewReader(body)), uri.String()).FirstForm().Testing(t).NewTestRequest(
|
||||||
append([]gosubmit.Option{gosubmit.AutoFill()}, opts...)...,
|
append([]gosubmit.Option{gosubmit.AutoFill()}, opts...)...,
|
||||||
)
|
)
|
||||||
if req.URL.Scheme == "" {
|
if req.URL.Scheme == "" {
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
"github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JWTProfileExchange handles the oauth2 jwt profile exchange
|
// JWTProfileExchange handles the oauth2 jwt profile exchange
|
||||||
func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller TokenEndpointCaller) (*oauth2.Token, error) {
|
func JWTProfileExchange(jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller TokenEndpointCaller) (*oauth2.Token, error) {
|
||||||
return CallTokenEndpoint(ctx, jwtProfileGrantRequest, caller)
|
return CallTokenEndpoint(jwtProfileGrantRequest, caller)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption {
|
func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption {
|
||||||
|
|
|
@ -2,7 +2,7 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -10,7 +10,7 @@ const (
|
||||||
applicationKey = "application"
|
applicationKey = "application"
|
||||||
)
|
)
|
||||||
|
|
||||||
type KeyFile struct {
|
type keyFile struct {
|
||||||
Type string `json:"type"` // serviceaccount or application
|
Type string `json:"type"` // serviceaccount or application
|
||||||
KeyID string `json:"keyId"`
|
KeyID string `json:"keyId"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
@ -23,16 +23,16 @@ type KeyFile struct {
|
||||||
ClientID string `json:"clientId"`
|
ClientID string `json:"clientId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConfigFromKeyFile(path string) (*KeyFile, error) {
|
func ConfigFromKeyFile(path string) (*keyFile, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ConfigFromKeyFileData(data)
|
return ConfigFromKeyFileData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConfigFromKeyFileData(data []byte) (*KeyFile, error) {
|
func ConfigFromKeyFileData(data []byte) (*keyFile, error) {
|
||||||
var f KeyFile
|
var f keyFile
|
||||||
if err := json.Unmarshal(data, &f); err != nil {
|
if err := json.Unmarshal(data, &f); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,19 @@
|
||||||
package profile
|
package profile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
"github.com/zitadel/oidc/v2/pkg/client"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TokenSource interface {
|
|
||||||
oauth2.TokenSource
|
|
||||||
TokenCtx(context.Context) (*oauth2.Token, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// jwtProfileTokenSource implement the oauth2.TokenSource
|
// jwtProfileTokenSource implement the oauth2.TokenSource
|
||||||
// it will request a token using the OAuth2 JWT Profile Grant
|
// it will request a token using the OAuth2 JWT Profile Grant
|
||||||
// therefore sending an `assertion` by signing a JWT with the provided private key
|
// therefore sending an `assertion` by singing a JWT with the provided private key
|
||||||
type jwtProfileTokenSource struct {
|
type jwtProfileTokenSource struct {
|
||||||
clientID string
|
clientID string
|
||||||
audience []string
|
audience []string
|
||||||
|
@ -29,38 +23,23 @@ type jwtProfileTokenSource struct {
|
||||||
tokenEndpoint string
|
tokenEndpoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJWTProfileTokenSourceFromKeyFile returns an implementation of TokenSource
|
func NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath string, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) {
|
||||||
// It will request a token using the OAuth2 JWT Profile Grant,
|
keyData, err := client.ConfigFromKeyFile(keyPath)
|
||||||
// therefore sending an `assertion` by singing a JWT with the provided private key from jsonFile.
|
|
||||||
//
|
|
||||||
// The passed context is only used for the call to the Discover endpoint.
|
|
||||||
func NewJWTProfileTokenSourceFromKeyFile(ctx context.Context, issuer, jsonFile string, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) {
|
|
||||||
keyData, err := client.ConfigFromKeyFile(jsonFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewJWTProfileTokenSource(ctx, issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
|
return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJWTProfileTokenSourceFromKeyFileData returns an implementation of oauth2.TokenSource
|
func NewJWTProfileTokenSourceFromKeyFileData(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) {
|
||||||
// It will request a token using the OAuth2 JWT Profile Grant,
|
keyData, err := client.ConfigFromKeyFileData(data)
|
||||||
// therefore sending an `assertion` by singing a JWT with the provided private key in jsonData.
|
|
||||||
//
|
|
||||||
// The passed context is only used for the call to the Discover endpoint.
|
|
||||||
func NewJWTProfileTokenSourceFromKeyFileData(ctx context.Context, issuer string, jsonData []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) {
|
|
||||||
keyData, err := client.ConfigFromKeyFileData(jsonData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewJWTProfileTokenSource(ctx, issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
|
return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJWTProfileSource returns an implementation of oauth2.TokenSource
|
func NewJWTProfileTokenSource(issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) {
|
||||||
// It will request a token using the OAuth2 JWT Profile Grant,
|
|
||||||
// therefore sending an `assertion` by singing a JWT with the provided private key.
|
|
||||||
//
|
|
||||||
// The passed context is only used for the call to the Discover endpoint.
|
|
||||||
func NewJWTProfileTokenSource(ctx context.Context, issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) {
|
|
||||||
signer, err := client.NewSignerFromPrivateKeyByte(key, keyID)
|
signer, err := client.NewSignerFromPrivateKeyByte(key, keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -76,7 +55,7 @@ func NewJWTProfileTokenSource(ctx context.Context, issuer, clientID, keyID strin
|
||||||
opt(source)
|
opt(source)
|
||||||
}
|
}
|
||||||
if source.tokenEndpoint == "" {
|
if source.tokenEndpoint == "" {
|
||||||
config, err := client.Discover(ctx, issuer, source.httpClient)
|
config, err := client.Discover(issuer, source.httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -85,13 +64,13 @@ func NewJWTProfileTokenSource(ctx context.Context, issuer, clientID, keyID strin
|
||||||
return source, nil
|
return source, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithHTTPClient(client *http.Client) func(source *jwtProfileTokenSource) {
|
func WithHTTPClient(client *http.Client) func(*jwtProfileTokenSource) {
|
||||||
return func(source *jwtProfileTokenSource) {
|
return func(source *jwtProfileTokenSource) {
|
||||||
source.httpClient = client
|
source.httpClient = client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(source *jwtProfileTokenSource) {
|
func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*jwtProfileTokenSource) {
|
||||||
return func(source *jwtProfileTokenSource) {
|
return func(source *jwtProfileTokenSource) {
|
||||||
source.tokenEndpoint = tokenEndpoint
|
source.tokenEndpoint = tokenEndpoint
|
||||||
}
|
}
|
||||||
|
@ -106,13 +85,9 @@ func (j *jwtProfileTokenSource) HttpClient() *http.Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) {
|
func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) {
|
||||||
return j.TokenCtx(context.Background())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileTokenSource) TokenCtx(ctx context.Context) (*oauth2.Token, error) {
|
|
||||||
assertion, err := client.SignedJWTProfileAssertion(j.clientID, j.audience, time.Hour, j.signer)
|
assertion, err := client.SignedJWTProfileAssertion(j.clientID, j.audience, time.Hour, j.signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return client.JWTProfileExchange(ctx, oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j)
|
return client.JWTProfileExchange(oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package rp
|
package rp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc/grants/tokenexchange"
|
"github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DelegationTokenRequest is an implementation of TokenExchangeRequest
|
// DelegationTokenRequest is an implementation of TokenExchangeRequest
|
||||||
|
|
|
@ -5,13 +5,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
"github.com/zitadel/oidc/v2/pkg/client"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) {
|
func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) {
|
||||||
confg := rp.OAuthConfig()
|
confg := rp.OAuthConfig()
|
||||||
req := &oidc.ClientCredentialsRequest{
|
req := &oidc.ClientCredentialsRequest{
|
||||||
|
GrantType: oidc.GrantTypeDeviceCode,
|
||||||
Scope: scopes,
|
Scope: scopes,
|
||||||
ClientID: confg.ClientID,
|
ClientID: confg.ClientID,
|
||||||
ClientSecret: confg.ClientSecret,
|
ClientSecret: confg.ClientSecret,
|
||||||
|
@ -32,27 +33,19 @@ func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.
|
||||||
// DeviceAuthorization starts a new Device Authorization flow as defined
|
// DeviceAuthorization starts a new Device Authorization flow as defined
|
||||||
// in RFC 8628, section 3.1 and 3.2:
|
// in RFC 8628, section 3.1 and 3.2:
|
||||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1
|
||||||
func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, authFn any) (*oidc.DeviceAuthorizationResponse, error) {
|
func DeviceAuthorization(scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) {
|
||||||
ctx, span := client.Tracer.Start(ctx, "DeviceAuthorization")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAuthorization")
|
|
||||||
req, err := newDeviceClientCredentialsRequest(scopes, rp)
|
req, err := newDeviceClientCredentialsRequest(scopes, rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.CallDeviceAuthorizationEndpoint(ctx, req, rp, authFn)
|
return client.CallDeviceAuthorizationEndpoint(req, rp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceAccessToken attempts to obtain tokens from a Device Authorization,
|
// DeviceAccessToken attempts to obtain tokens from a Device Authorization,
|
||||||
// by means of polling as defined in RFC, section 3.3 and 3.4:
|
// by means of polling as defined in RFC, section 3.3 and 3.4:
|
||||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4
|
||||||
func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) {
|
func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) {
|
||||||
ctx, span := client.Tracer.Start(ctx, "DeviceAccessToken")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAccessToken")
|
|
||||||
req := &client.DeviceAccessTokenRequest{
|
req := &client.DeviceAccessTokenRequest{
|
||||||
DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{
|
DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{
|
||||||
GrantType: oidc.GrantTypeDeviceCode,
|
GrantType: oidc.GrantTypeDeviceCode,
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
package rp
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var ErrRelyingPartyNotSupportRevokeCaller = errors.New("RelyingParty does not support RevokeCaller")
|
|
|
@ -7,11 +7,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet {
|
func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet {
|
||||||
|
@ -84,9 +83,6 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
|
func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
|
||||||
ctx, span := client.Tracer.Start(ctx, "VerifySignature")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
keyID, alg := oidc.GetKeyIDAndAlg(jws)
|
keyID, alg := oidc.GetKeyIDAndAlg(jws)
|
||||||
if alg == "" {
|
if alg == "" {
|
||||||
alg = r.defaultAlg
|
alg = r.defaultAlg
|
||||||
|
@ -139,9 +135,6 @@ func (r *remoteKeySet) exactMatch(jwkID, jwsID string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) {
|
func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) {
|
||||||
ctx, span := client.Tracer.Start(ctx, "verifySignatureRemote")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
keys, err := r.keysFromRemote(ctx)
|
keys, err := r.keysFromRemote(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to fetch key for signature validation: %w", err)
|
return nil, fmt.Errorf("unable to fetch key for signature validation: %w", err)
|
||||||
|
@ -166,9 +159,6 @@ func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey) {
|
||||||
// keysFromRemote syncs the key set from the remote set, records the values in the
|
// keysFromRemote syncs the key set from the remote set, records the values in the
|
||||||
// cache, and returns the key set.
|
// cache, and returns the key set.
|
||||||
func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) {
|
func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) {
|
||||||
ctx, span := client.Tracer.Start(ctx, "keysFromRemote")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
// Need to lock to inspect the inflight request field.
|
// Need to lock to inspect the inflight request field.
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
// If there's not a current inflight request, create one.
|
// If there's not a current inflight request, create one.
|
||||||
|
@ -192,9 +182,6 @@ func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteKeySet) updateKeys(ctx context.Context) {
|
func (r *remoteKeySet) updateKeys(ctx context.Context) {
|
||||||
ctx, span := client.Tracer.Start(ctx, "updateKeys")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
// Sync keys and finish inflight when that's done.
|
// Sync keys and finish inflight when that's done.
|
||||||
keys, err := r.fetchRemoteKeys(ctx)
|
keys, err := r.fetchRemoteKeys(ctx)
|
||||||
|
|
||||||
|
@ -214,10 +201,7 @@ func (r *remoteKeySet) updateKeys(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, error) {
|
func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, error) {
|
||||||
ctx, span := client.Tracer.Start(ctx, "fetchRemoteKeys")
|
req, err := http.NewRequest("GET", r.jwksURL, nil)
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", r.jwksURL, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("oidc: can't create request: %v", err)
|
return nil, fmt.Errorf("oidc: can't create request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
package rp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"github.com/zitadel/logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
func logCtxWithRPData(ctx context.Context, rp RelyingParty, attrs ...any) context.Context {
|
|
||||||
logger, ok := rp.Logger(ctx)
|
|
||||||
if !ok {
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
logger = logger.With(slog.Group("rp", attrs...))
|
|
||||||
return logging.ToContext(ctx, logger)
|
|
||||||
}
|
|
|
@ -4,20 +4,19 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-jose/go-jose/v4"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/clientcredentials"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
"github.com/zitadel/oidc/v2/pkg/client"
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"github.com/zitadel/logging"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -60,55 +59,38 @@ type RelyingParty interface {
|
||||||
// UserinfoEndpoint returns the userinfo
|
// UserinfoEndpoint returns the userinfo
|
||||||
UserinfoEndpoint() string
|
UserinfoEndpoint() string
|
||||||
|
|
||||||
// GetDeviceAuthorizationEndpoint returns the endpoint which can
|
// GetDeviceAuthorizationEndpoint returns the enpoint which can
|
||||||
// be used to start a DeviceAuthorization flow.
|
// be used to start a DeviceAuthorization flow.
|
||||||
GetDeviceAuthorizationEndpoint() string
|
GetDeviceAuthorizationEndpoint() string
|
||||||
|
|
||||||
// IDTokenVerifier returns the verifier used for oidc id_token verification
|
// IDTokenVerifier returns the verifier interface used for oidc id_token verification
|
||||||
IDTokenVerifier() *IDTokenVerifier
|
IDTokenVerifier() IDTokenVerifier
|
||||||
|
|
||||||
// ErrorHandler returns the handler used for callback errors
|
// ErrorHandler returns the handler used for callback errors
|
||||||
|
|
||||||
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
|
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
|
||||||
|
|
||||||
// Logger from the context, or a fallback if set.
|
|
||||||
Logger(context.Context) (logger *slog.Logger, ok bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
type HasUnauthorizedHandler interface {
|
|
||||||
// UnauthorizedHandler returns the handler used for unauthorized errors
|
|
||||||
UnauthorizedHandler() func(w http.ResponseWriter, r *http.Request, desc string, state string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string)
|
type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string)
|
||||||
type UnauthorizedHandler func(w http.ResponseWriter, r *http.Request, desc string, state string)
|
|
||||||
|
|
||||||
var DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
|
var DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
|
||||||
http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError)
|
http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
var DefaultUnauthorizedHandler UnauthorizedHandler = func(w http.ResponseWriter, r *http.Request, desc string, state string) {
|
|
||||||
http.Error(w, desc, http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
type relyingParty struct {
|
type relyingParty struct {
|
||||||
issuer string
|
issuer string
|
||||||
DiscoveryEndpoint string
|
DiscoveryEndpoint string
|
||||||
endpoints Endpoints
|
endpoints Endpoints
|
||||||
oauthConfig *oauth2.Config
|
oauthConfig *oauth2.Config
|
||||||
oauth2Only bool
|
oauth2Only bool
|
||||||
pkce bool
|
pkce bool
|
||||||
useSigningAlgsFromDiscovery bool
|
|
||||||
|
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
cookieHandler *httphelper.CookieHandler
|
cookieHandler *httphelper.CookieHandler
|
||||||
|
|
||||||
oauthAuthStyle oauth2.AuthStyle
|
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
|
||||||
|
idTokenVerifier IDTokenVerifier
|
||||||
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
|
verifierOpts []VerifierOption
|
||||||
unauthorizedHandler func(http.ResponseWriter, *http.Request, string, string)
|
signer jose.Signer
|
||||||
idTokenVerifier *IDTokenVerifier
|
|
||||||
verifierOpts []VerifierOption
|
|
||||||
signer jose.Signer
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *relyingParty) OAuthConfig() *oauth2.Config {
|
func (rp *relyingParty) OAuthConfig() *oauth2.Config {
|
||||||
|
@ -155,7 +137,7 @@ func (rp *relyingParty) GetRevokeEndpoint() string {
|
||||||
return rp.endpoints.RevokeURL
|
return rp.endpoints.RevokeURL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *relyingParty) IDTokenVerifier() *IDTokenVerifier {
|
func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier {
|
||||||
if rp.idTokenVerifier == nil {
|
if rp.idTokenVerifier == nil {
|
||||||
rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...)
|
rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...)
|
||||||
}
|
}
|
||||||
|
@ -169,31 +151,14 @@ func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request,
|
||||||
return rp.errorHandler
|
return rp.errorHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *relyingParty) UnauthorizedHandler() func(http.ResponseWriter, *http.Request, string, string) {
|
|
||||||
if rp.unauthorizedHandler == nil {
|
|
||||||
rp.unauthorizedHandler = DefaultUnauthorizedHandler
|
|
||||||
}
|
|
||||||
return rp.unauthorizedHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rp *relyingParty) Logger(ctx context.Context) (logger *slog.Logger, ok bool) {
|
|
||||||
logger, ok = logging.FromContext(ctx)
|
|
||||||
if ok {
|
|
||||||
return logger, ok
|
|
||||||
}
|
|
||||||
return rp.logger, rp.logger != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given
|
// NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given
|
||||||
// OAuth2 Config and possible configOptions
|
// OAuth2 Config and possible configOptions
|
||||||
// it will use the AuthURL and TokenURL set in config
|
// it will use the AuthURL and TokenURL set in config
|
||||||
func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) {
|
func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) {
|
||||||
rp := &relyingParty{
|
rp := &relyingParty{
|
||||||
oauthConfig: config,
|
oauthConfig: config,
|
||||||
httpClient: httphelper.DefaultHTTPClient,
|
httpClient: httphelper.DefaultHTTPClient,
|
||||||
oauth2Only: true,
|
oauth2Only: true,
|
||||||
unauthorizedHandler: DefaultUnauthorizedHandler,
|
|
||||||
oauthAuthStyle: oauth2.AuthStyleAutoDetect,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, optFunc := range options {
|
for _, optFunc := range options {
|
||||||
|
@ -202,12 +167,9 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rp.oauthConfig.Endpoint.AuthStyle = rp.oauthAuthStyle
|
|
||||||
|
|
||||||
// avoid races by calling these early
|
// avoid races by calling these early
|
||||||
_ = rp.IDTokenVerifier() // sets idTokenVerifier
|
_ = rp.IDTokenVerifier() // sets idTokenVerifier
|
||||||
_ = rp.ErrorHandler() // sets errorHandler
|
_ = rp.ErrorHandler() // sets errorHandler
|
||||||
_ = rp.UnauthorizedHandler() // sets unauthorizedHandler
|
|
||||||
|
|
||||||
return rp, nil
|
return rp, nil
|
||||||
}
|
}
|
||||||
|
@ -215,7 +177,7 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart
|
||||||
// NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given
|
// NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given
|
||||||
// issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions
|
// issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions
|
||||||
// it will run discovery on the provided issuer and use the found endpoints
|
// it will run discovery on the provided issuer and use the found endpoints
|
||||||
func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) {
|
func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) {
|
||||||
rp := &relyingParty{
|
rp := &relyingParty{
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
oauthConfig: &oauth2.Config{
|
oauthConfig: &oauth2.Config{
|
||||||
|
@ -224,9 +186,8 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
|
||||||
RedirectURL: redirectURI,
|
RedirectURL: redirectURI,
|
||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
},
|
},
|
||||||
httpClient: httphelper.DefaultHTTPClient,
|
httpClient: httphelper.DefaultHTTPClient,
|
||||||
oauth2Only: false,
|
oauth2Only: false,
|
||||||
oauthAuthStyle: oauth2.AuthStyleAutoDetect,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, optFunc := range options {
|
for _, optFunc := range options {
|
||||||
|
@ -234,25 +195,17 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx = logCtxWithRPData(ctx, rp, "function", "NewRelyingPartyOIDC")
|
discoveryConfiguration, err := client.Discover(rp.issuer, rp.httpClient, rp.DiscoveryEndpoint)
|
||||||
discoveryConfiguration, err := client.Discover(ctx, rp.issuer, rp.httpClient, rp.DiscoveryEndpoint)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if rp.useSigningAlgsFromDiscovery {
|
|
||||||
rp.verifierOpts = append(rp.verifierOpts, WithSupportedSigningAlgorithms(discoveryConfiguration.IDTokenSigningAlgValuesSupported...))
|
|
||||||
}
|
|
||||||
endpoints := GetEndpoints(discoveryConfiguration)
|
endpoints := GetEndpoints(discoveryConfiguration)
|
||||||
rp.oauthConfig.Endpoint = endpoints.Endpoint
|
rp.oauthConfig.Endpoint = endpoints.Endpoint
|
||||||
rp.endpoints = endpoints
|
rp.endpoints = endpoints
|
||||||
|
|
||||||
rp.oauthConfig.Endpoint.AuthStyle = rp.oauthAuthStyle
|
|
||||||
rp.endpoints.Endpoint.AuthStyle = rp.oauthAuthStyle
|
|
||||||
|
|
||||||
// avoid races by calling these early
|
// avoid races by calling these early
|
||||||
_ = rp.IDTokenVerifier() // sets idTokenVerifier
|
_ = rp.IDTokenVerifier() // sets idTokenVerifier
|
||||||
_ = rp.ErrorHandler() // sets errorHandler
|
_ = rp.ErrorHandler() // sets errorHandler
|
||||||
_ = rp.UnauthorizedHandler() // sets unauthorizedHandler
|
|
||||||
|
|
||||||
return rp, nil
|
return rp, nil
|
||||||
}
|
}
|
||||||
|
@ -301,20 +254,6 @@ func WithErrorHandler(errorHandler ErrorHandler) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithUnauthorizedHandler(unauthorizedHandler UnauthorizedHandler) Option {
|
|
||||||
return func(rp *relyingParty) error {
|
|
||||||
rp.unauthorizedHandler = unauthorizedHandler
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithAuthStyle(oauthAuthStyle oauth2.AuthStyle) Option {
|
|
||||||
return func(rp *relyingParty) error {
|
|
||||||
rp.oauthAuthStyle = oauthAuthStyle
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithVerifierOpts(opts ...VerifierOption) Option {
|
func WithVerifierOpts(opts ...VerifierOption) Option {
|
||||||
return func(rp *relyingParty) error {
|
return func(rp *relyingParty) error {
|
||||||
rp.verifierOpts = opts
|
rp.verifierOpts = opts
|
||||||
|
@ -343,24 +282,6 @@ func WithJWTProfile(signerFromKey SignerFromKey) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithLogger sets a logger that is used
|
|
||||||
// in case the request context does not contain a logger.
|
|
||||||
func WithLogger(logger *slog.Logger) Option {
|
|
||||||
return func(rp *relyingParty) error {
|
|
||||||
rp.logger = logger
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSigningAlgsFromDiscovery appends the [WithSupportedSigningAlgorithms] option to the Verifier Options.
|
|
||||||
// The algorithms returned in the `id_token_signing_alg_values_supported` from the discovery response will be set.
|
|
||||||
func WithSigningAlgsFromDiscovery() Option {
|
|
||||||
return func(rp *relyingParty) error {
|
|
||||||
rp.useSigningAlgsFromDiscovery = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SignerFromKey func() (jose.Signer, error)
|
type SignerFromKey func() (jose.Signer, error)
|
||||||
|
|
||||||
func SignerFromKeyPath(path string) SignerFromKey {
|
func SignerFromKeyPath(path string) SignerFromKey {
|
||||||
|
@ -389,6 +310,26 @@ func SignerFromKeyAndKeyID(key []byte, keyID string) SignerFromKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Discover calls the discovery endpoint of the provided issuer and returns the found endpoints
|
||||||
|
//
|
||||||
|
// deprecated: use client.Discover
|
||||||
|
func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
|
||||||
|
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
|
||||||
|
req, err := http.NewRequest("GET", wellKnown, nil)
|
||||||
|
if err != nil {
|
||||||
|
return Endpoints{}, err
|
||||||
|
}
|
||||||
|
discoveryConfig := new(oidc.DiscoveryConfiguration)
|
||||||
|
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
|
||||||
|
if err != nil {
|
||||||
|
return Endpoints{}, err
|
||||||
|
}
|
||||||
|
if discoveryConfig.Issuer != issuer {
|
||||||
|
return Endpoints{}, oidc.ErrIssuerInvalid
|
||||||
|
}
|
||||||
|
return GetEndpoints(discoveryConfig), nil
|
||||||
|
}
|
||||||
|
|
||||||
// AuthURL returns the auth request url
|
// AuthURL returns the auth request url
|
||||||
// (wrapping the oauth2 `AuthCodeURL`)
|
// (wrapping the oauth2 `AuthCodeURL`)
|
||||||
func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string {
|
func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string {
|
||||||
|
@ -401,7 +342,7 @@ func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string {
|
||||||
|
|
||||||
// AuthURLHandler extends the `AuthURL` method with a http redirect handler
|
// AuthURLHandler extends the `AuthURL` method with a http redirect handler
|
||||||
// including handling setting cookie for secure `state` transfer.
|
// including handling setting cookie for secure `state` transfer.
|
||||||
// Custom parameters can optionally be set to the redirect URL.
|
// Custom paramaters can optionally be set to the redirect URL.
|
||||||
func AuthURLHandler(stateFn func() string, rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
|
func AuthURLHandler(stateFn func() string, rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
opts := make([]AuthURLOpt, len(urlParam))
|
opts := make([]AuthURLOpt, len(urlParam))
|
||||||
|
@ -411,13 +352,13 @@ func AuthURLHandler(stateFn func() string, rp RelyingParty, urlParam ...URLParam
|
||||||
|
|
||||||
state := stateFn()
|
state := stateFn()
|
||||||
if err := trySetStateCookie(w, state, rp); err != nil {
|
if err := trySetStateCookie(w, state, rp); err != nil {
|
||||||
unauthorizedError(w, r, "failed to create state cookie: "+err.Error(), state, rp)
|
http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if rp.IsPKCE() {
|
if rp.IsPKCE() {
|
||||||
codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp)
|
codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
unauthorizedError(w, r, "failed to create code challenge: "+err.Error(), state, rp)
|
http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
opts = append(opts, WithCodeChallenge(codeChallenge))
|
opts = append(opts, WithCodeChallenge(codeChallenge))
|
||||||
|
@ -436,73 +377,35 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri
|
||||||
return oidc.NewSHACodeChallenge(codeVerifier), nil
|
return oidc.NewSHACodeChallenge(codeVerifier), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrMissingIDToken is returned when an id_token was expected,
|
|
||||||
// but not received in the token response.
|
|
||||||
var ErrMissingIDToken = errors.New("id_token missing")
|
|
||||||
|
|
||||||
func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Token, rp RelyingParty) (*oidc.Tokens[C], error) {
|
|
||||||
ctx, span := client.Tracer.Start(ctx, "verifyTokenResponse")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
if rp.IsOAuth2Only() {
|
|
||||||
return &oidc.Tokens[C]{Token: token}, nil
|
|
||||||
}
|
|
||||||
idTokenString, ok := token.Extra(idTokenKey).(string)
|
|
||||||
if !ok {
|
|
||||||
return &oidc.Tokens[C]{Token: token}, ErrMissingIDToken
|
|
||||||
}
|
|
||||||
idToken, err := VerifyTokens[C](ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &oidc.Tokens[C]{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CodeExchange handles the oauth2 code exchange, extracting and validating the id_token
|
// CodeExchange handles the oauth2 code exchange, extracting and validating the id_token
|
||||||
// returning it parsed together with the oauth2 tokens (access, refresh)
|
// returning it parsed together with the oauth2 tokens (access, refresh)
|
||||||
func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) {
|
func CodeExchange[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())
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
|
||||||
codeOpts := make([]oauth2.AuthCodeOption, 0)
|
codeOpts := make([]oauth2.AuthCodeOption, 0)
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
codeOpts = append(codeOpts, opt()...)
|
codeOpts = append(codeOpts, opt()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, oauthExchangeSpan := client.Tracer.Start(ctx, "OAuthExchange")
|
|
||||||
token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...)
|
token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
oauthExchangeSpan.End()
|
|
||||||
return verifyTokenResponse[C](ctx, token, rp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientCredentials requests an access token using the `client_credentials` grant,
|
if rp.IsOAuth2Only() {
|
||||||
// as defined in [RFC 6749, section 4.4].
|
return &oidc.Tokens[C]{Token: token}, nil
|
||||||
//
|
|
||||||
// As there is no user associated to the request an ID Token can never be returned.
|
|
||||||
// Client Credentials are undefined in OpenID Connect and is a pure OAuth2 grant.
|
|
||||||
// Furthermore the server SHOULD NOT return a refresh token.
|
|
||||||
//
|
|
||||||
// [RFC 6749, section 4.4]: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
|
|
||||||
func ClientCredentials(ctx context.Context, rp RelyingParty, endpointParams url.Values) (token *oauth2.Token, err error) {
|
|
||||||
ctx = logCtxWithRPData(ctx, rp, "function", "ClientCredentials")
|
|
||||||
ctx, span := client.Tracer.Start(ctx, "ClientCredentials")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
|
|
||||||
config := clientcredentials.Config{
|
|
||||||
ClientID: rp.OAuthConfig().ClientID,
|
|
||||||
ClientSecret: rp.OAuthConfig().ClientSecret,
|
|
||||||
TokenURL: rp.OAuthConfig().Endpoint.TokenURL,
|
|
||||||
Scopes: rp.OAuthConfig().Scopes,
|
|
||||||
EndpointParams: endpointParams,
|
|
||||||
AuthStyle: rp.OAuthConfig().Endpoint.AuthStyle,
|
|
||||||
}
|
}
|
||||||
return config.Token(ctx)
|
|
||||||
|
idTokenString, ok := token.Extra(idTokenKey).(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("id_token missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := VerifyTokens[C](ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &oidc.Tokens[C]{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty)
|
type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty)
|
||||||
|
@ -510,20 +413,17 @@ type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.R
|
||||||
// CodeExchangeHandler extends the `CodeExchange` method with a http handler
|
// CodeExchangeHandler extends the `CodeExchange` method with a http handler
|
||||||
// including cookie handling for secure `state` transfer
|
// including cookie handling for secure `state` transfer
|
||||||
// and optional PKCE code verifier checking.
|
// and optional PKCE code verifier checking.
|
||||||
// Custom parameters can optionally be set to the token URL.
|
// Custom paramaters can optionally be set to the token URL.
|
||||||
func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
|
func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx, span := client.Tracer.Start(r.Context(), "CodeExchangeHandler")
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
state, err := tryReadStateCookie(w, r, rp)
|
state, err := tryReadStateCookie(w, r, rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
unauthorizedError(w, r, "failed to get state: "+err.Error(), state, rp)
|
http.Error(w, "failed to get state: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errValue := r.FormValue("error"); errValue != "" {
|
params := r.URL.Query()
|
||||||
rp.ErrorHandler()(w, r, errValue, r.FormValue("error_description"), state)
|
if params.Get("error") != "" {
|
||||||
|
rp.ErrorHandler()(w, r, params.Get("error"), params.Get("error_description"), state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
codeOpts := make([]CodeExchangeOpt, len(urlParam))
|
codeOpts := make([]CodeExchangeOpt, len(urlParam))
|
||||||
|
@ -534,75 +434,57 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R
|
||||||
if rp.IsPKCE() {
|
if rp.IsPKCE() {
|
||||||
codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode)
|
codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
unauthorizedError(w, r, "failed to get code verifier: "+err.Error(), state, rp)
|
http.Error(w, "failed to get code verifier: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
|
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
|
||||||
rp.CookieHandler().DeleteCookie(w, pkceCode)
|
|
||||||
}
|
}
|
||||||
if rp.Signer() != nil {
|
if rp.Signer() != nil {
|
||||||
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer(), rp.OAuthConfig().Endpoint.TokenURL}, time.Hour, rp.Signer())
|
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
unauthorizedError(w, r, "failed to build assertion: "+err.Error(), state, rp)
|
http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
|
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
|
||||||
}
|
}
|
||||||
tokens, err := CodeExchange[C](r.Context(), r.FormValue("code"), rp, codeOpts...)
|
tokens, err := CodeExchange[C](r.Context(), params.Get("code"), rp, codeOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
unauthorizedError(w, r, "failed to exchange token: "+err.Error(), state, rp)
|
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
callback(w, r, tokens, state, rp)
|
callback(w, r, tokens, state, rp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectGetter interface {
|
type CodeExchangeUserinfoCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info *oidc.UserInfo)
|
||||||
GetSubject() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CodeExchangeUserinfoCallback[C oidc.IDClaims, U SubjectGetter] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info U)
|
|
||||||
|
|
||||||
// UserinfoCallback wraps the callback function of the CodeExchangeHandler
|
// UserinfoCallback wraps the callback function of the CodeExchangeHandler
|
||||||
// and calls the userinfo endpoint with the access token
|
// and calls the userinfo endpoint with the access token
|
||||||
// on success it will pass the userinfo into its callback function as well
|
// on success it will pass the userinfo into its callback function as well
|
||||||
func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCallback[C, U]) CodeExchangeCallback[C] {
|
func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeExchangeCallback[C] {
|
||||||
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) {
|
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) {
|
||||||
ctx, span := client.Tracer.Start(r.Context(), "UserinfoCallback")
|
info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
||||||
r = r.WithContext(ctx)
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
unauthorizedError(w, r, "userinfo failed: "+err.Error(), state, rp)
|
http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f(w, r, tokens, state, rp, info)
|
f(w, r, tokens, state, rp, info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Userinfo will call the OIDC [UserInfo] Endpoint with the provided token and returns
|
// Userinfo will call the OIDC Userinfo Endpoint with the provided token
|
||||||
// the response in an instance of type U.
|
func Userinfo(token, tokenType, subject string, rp RelyingParty) (*oidc.UserInfo, error) {
|
||||||
// [*oidc.UserInfo] can be used as a good example, or use a custom type if type-safe
|
req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil)
|
||||||
// access to custom claims is needed.
|
|
||||||
//
|
|
||||||
// [UserInfo]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
|
||||||
func Userinfo[U SubjectGetter](ctx context.Context, token, tokenType, subject string, rp RelyingParty) (userinfo U, err error) {
|
|
||||||
var nilU U
|
|
||||||
ctx = logCtxWithRPData(ctx, rp, "function", "Userinfo")
|
|
||||||
ctx, span := client.Tracer.Start(ctx, "Userinfo")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nilU, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("authorization", tokenType+" "+token)
|
req.Header.Set("authorization", tokenType+" "+token)
|
||||||
|
userinfo := new(oidc.UserInfo)
|
||||||
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
|
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
|
||||||
return nilU, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if userinfo.GetSubject() != subject {
|
if userinfo.Subject != subject {
|
||||||
return nilU, ErrUserInfoSubNotMatching
|
return nil, ErrUserInfoSubNotMatching
|
||||||
}
|
}
|
||||||
return userinfo, nil
|
return userinfo, nil
|
||||||
}
|
}
|
||||||
|
@ -643,8 +525,9 @@ type Endpoints struct {
|
||||||
func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
|
func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
|
||||||
return Endpoints{
|
return Endpoints{
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
AuthURL: discoveryConfig.AuthorizationEndpoint,
|
AuthURL: discoveryConfig.AuthorizationEndpoint,
|
||||||
TokenURL: discoveryConfig.TokenEndpoint,
|
AuthStyle: oauth2.AuthStyleAutoDetect,
|
||||||
|
TokenURL: discoveryConfig.TokenEndpoint,
|
||||||
},
|
},
|
||||||
IntrospectURL: discoveryConfig.IntrospectionEndpoint,
|
IntrospectURL: discoveryConfig.IntrospectionEndpoint,
|
||||||
UserinfoURL: discoveryConfig.UserinfoEndpoint,
|
UserinfoURL: discoveryConfig.UserinfoEndpoint,
|
||||||
|
@ -655,7 +538,7 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// withURLParam sets custom url parameters.
|
// withURLParam sets custom url paramaters.
|
||||||
// This is the generalized, unexported, function used by both
|
// This is the generalized, unexported, function used by both
|
||||||
// URLParamOpt and AuthURLOpt.
|
// URLParamOpt and AuthURLOpt.
|
||||||
func withURLParam(key, value string) func() []oauth2.AuthCodeOption {
|
func withURLParam(key, value string) func() []oauth2.AuthCodeOption {
|
||||||
|
@ -670,7 +553,7 @@ func withURLParam(key, value string) func() []oauth2.AuthCodeOption {
|
||||||
// This is the generalized, unexported, function used by both
|
// This is the generalized, unexported, function used by both
|
||||||
// URLParamOpt and AuthURLOpt.
|
// URLParamOpt and AuthURLOpt.
|
||||||
func withPrompt(prompt ...string) func() []oauth2.AuthCodeOption {
|
func withPrompt(prompt ...string) func() []oauth2.AuthCodeOption {
|
||||||
return withURLParam("prompt", oidc.SpaceDelimitedArray(prompt).String())
|
return withURLParam("prompt", oidc.SpaceDelimitedArray(prompt).Encode())
|
||||||
}
|
}
|
||||||
|
|
||||||
type URLParamOpt func() []oauth2.AuthCodeOption
|
type URLParamOpt func() []oauth2.AuthCodeOption
|
||||||
|
@ -686,11 +569,6 @@ func WithPromptURLParam(prompt ...string) URLParamOpt {
|
||||||
return withPrompt(prompt...)
|
return withPrompt(prompt...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithResponseModeURLParam sets the `response_mode` parameter in a URL.
|
|
||||||
func WithResponseModeURLParam(mode oidc.ResponseMode) URLParamOpt {
|
|
||||||
return withURLParam("response_mode", string(mode))
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthURLOpt func() []oauth2.AuthCodeOption
|
type AuthURLOpt func() []oauth2.AuthCodeOption
|
||||||
|
|
||||||
// WithCodeChallenge sets the `code_challenge` params in the auth request
|
// WithCodeChallenge sets the `code_challenge` params in the auth request
|
||||||
|
@ -734,26 +612,15 @@ func (t tokenEndpointCaller) TokenEndpoint() string {
|
||||||
|
|
||||||
type RefreshTokenRequest struct {
|
type RefreshTokenRequest struct {
|
||||||
RefreshToken string `schema:"refresh_token"`
|
RefreshToken string `schema:"refresh_token"`
|
||||||
Scopes oidc.SpaceDelimitedArray `schema:"scope,omitempty"`
|
Scopes oidc.SpaceDelimitedArray `schema:"scope"`
|
||||||
ClientID string `schema:"client_id,omitempty"`
|
ClientID string `schema:"client_id"`
|
||||||
ClientSecret string `schema:"client_secret,omitempty"`
|
ClientSecret string `schema:"client_secret"`
|
||||||
ClientAssertion string `schema:"client_assertion,omitempty"`
|
ClientAssertion string `schema:"client_assertion"`
|
||||||
ClientAssertionType string `schema:"client_assertion_type,omitempty"`
|
ClientAssertionType string `schema:"client_assertion_type"`
|
||||||
GrantType oidc.GrantType `schema:"grant_type"`
|
GrantType oidc.GrantType `schema:"grant_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshTokens performs a token refresh. If it doesn't error, it will always
|
func RefreshAccessToken(rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oauth2.Token, error) {
|
||||||
// provide a new AccessToken. It may provide a new RefreshToken, and if it does, then
|
|
||||||
// the old one should be considered invalid.
|
|
||||||
//
|
|
||||||
// In case the RP is not OAuth2 only and an IDToken was part of the response,
|
|
||||||
// the IDToken and AccessToken will be verified
|
|
||||||
// and the IDToken and IDTokenClaims fields will be populated in the returned object.
|
|
||||||
func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oidc.Tokens[C], error) {
|
|
||||||
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
ctx = logCtxWithRPData(ctx, rp, "function", "RefreshTokens")
|
|
||||||
request := RefreshTokenRequest{
|
request := RefreshTokenRequest{
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
Scopes: rp.OAuthConfig().Scopes,
|
Scopes: rp.OAuthConfig().Scopes,
|
||||||
|
@ -763,31 +630,17 @@ func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refres
|
||||||
ClientAssertionType: clientAssertionType,
|
ClientAssertionType: clientAssertionType,
|
||||||
GrantType: oidc.GrantTypeRefreshToken,
|
GrantType: oidc.GrantTypeRefreshToken,
|
||||||
}
|
}
|
||||||
newToken, err := client.CallTokenEndpoint(ctx, request, tokenEndpointCaller{RelyingParty: rp})
|
return client.CallTokenEndpoint(request, tokenEndpointCaller{RelyingParty: rp})
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tokens, err := verifyTokenResponse[C](ctx, newToken, rp)
|
|
||||||
if err == nil || errors.Is(err, ErrMissingIDToken) {
|
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
|
|
||||||
// ...except that it might not contain an id_token.
|
|
||||||
return tokens, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) {
|
func EndSession(rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) {
|
||||||
ctx = logCtxWithRPData(ctx, rp, "function", "EndSession")
|
|
||||||
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
request := oidc.EndSessionRequest{
|
request := oidc.EndSessionRequest{
|
||||||
IdTokenHint: idToken,
|
IdTokenHint: idToken,
|
||||||
ClientID: rp.OAuthConfig().ClientID,
|
ClientID: rp.OAuthConfig().ClientID,
|
||||||
PostLogoutRedirectURI: optionalRedirectURI,
|
PostLogoutRedirectURI: optionalRedirectURI,
|
||||||
State: optionalState,
|
State: optionalState,
|
||||||
}
|
}
|
||||||
return client.CallEndSessionEndpoint(ctx, request, nil, rp)
|
return client.CallEndSessionEndpoint(request, nil, rp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RevokeToken requires a RelyingParty that is also a client.RevokeCaller. The RelyingParty
|
// RevokeToken requires a RelyingParty that is also a client.RevokeCaller. The RelyingParty
|
||||||
|
@ -795,10 +648,7 @@ func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectU
|
||||||
// NewRelyingPartyOAuth() does not.
|
// NewRelyingPartyOAuth() does not.
|
||||||
//
|
//
|
||||||
// tokenTypeHint should be either "id_token" or "refresh_token".
|
// tokenTypeHint should be either "id_token" or "refresh_token".
|
||||||
func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHint string) error {
|
func RevokeToken(rp RelyingParty, token string, tokenTypeHint string) error {
|
||||||
ctx = logCtxWithRPData(ctx, rp, "function", "RevokeToken")
|
|
||||||
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
|
|
||||||
defer span.End()
|
|
||||||
request := client.RevokeRequest{
|
request := client.RevokeRequest{
|
||||||
Token: token,
|
Token: token,
|
||||||
TokenTypeHint: tokenTypeHint,
|
TokenTypeHint: tokenTypeHint,
|
||||||
|
@ -806,15 +656,7 @@ func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHi
|
||||||
ClientSecret: rp.OAuthConfig().ClientSecret,
|
ClientSecret: rp.OAuthConfig().ClientSecret,
|
||||||
}
|
}
|
||||||
if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" {
|
if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" {
|
||||||
return client.CallRevokeEndpoint(ctx, request, nil, rc)
|
return client.CallRevokeEndpoint(request, nil, rc)
|
||||||
}
|
}
|
||||||
return ErrRelyingPartyNotSupportRevokeCaller
|
return fmt.Errorf("RelyingParty does not support RevokeCaller")
|
||||||
}
|
|
||||||
|
|
||||||
func unauthorizedError(w http.ResponseWriter, r *http.Request, desc string, state string, rp RelyingParty) {
|
|
||||||
if rp, ok := rp.(HasUnauthorizedHandler); ok {
|
|
||||||
rp.UnauthorizedHandler()(w, r, desc, state)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, desc, http.StatusUnauthorized)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
package rp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_verifyTokenResponse(t *testing.T) {
|
|
||||||
verifier := &IDTokenVerifier{
|
|
||||||
Issuer: tu.ValidIssuer,
|
|
||||||
MaxAgeIAT: 2 * time.Minute,
|
|
||||||
ClientID: tu.ValidClientID,
|
|
||||||
Offset: time.Second,
|
|
||||||
SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
|
||||||
KeySet: tu.KeySet{},
|
|
||||||
MaxAge: 2 * time.Minute,
|
|
||||||
ACR: tu.ACRVerify,
|
|
||||||
Nonce: func(context.Context) string { return tu.ValidNonce },
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
oauth2Only bool
|
|
||||||
tokens func() (token *oauth2.Token, want *oidc.Tokens[*oidc.IDTokenClaims])
|
|
||||||
wantErr error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "succes, oauth2 only",
|
|
||||||
oauth2Only: true,
|
|
||||||
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
|
|
||||||
accesToken, _ := tu.ValidAccessToken()
|
|
||||||
token := &oauth2.Token{
|
|
||||||
AccessToken: accesToken,
|
|
||||||
}
|
|
||||||
return token, &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
||||||
Token: token,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "id_token missing error",
|
|
||||||
oauth2Only: false,
|
|
||||||
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
|
|
||||||
accesToken, _ := tu.ValidAccessToken()
|
|
||||||
token := &oauth2.Token{
|
|
||||||
AccessToken: accesToken,
|
|
||||||
}
|
|
||||||
return token, &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
||||||
Token: token,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantErr: ErrMissingIDToken,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "verify tokens error",
|
|
||||||
oauth2Only: false,
|
|
||||||
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
|
|
||||||
accesToken, _ := tu.ValidAccessToken()
|
|
||||||
token := &oauth2.Token{
|
|
||||||
AccessToken: accesToken,
|
|
||||||
}
|
|
||||||
token = token.WithExtra(map[string]any{
|
|
||||||
"id_token": "foobar",
|
|
||||||
})
|
|
||||||
return token, nil
|
|
||||||
},
|
|
||||||
wantErr: oidc.ErrParse,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success, with id_token",
|
|
||||||
oauth2Only: false,
|
|
||||||
tokens: func() (*oauth2.Token, *oidc.Tokens[*oidc.IDTokenClaims]) {
|
|
||||||
accesToken, _ := tu.ValidAccessToken()
|
|
||||||
token := &oauth2.Token{
|
|
||||||
AccessToken: accesToken,
|
|
||||||
}
|
|
||||||
idToken, claims := tu.ValidIDToken()
|
|
||||||
token = token.WithExtra(map[string]any{
|
|
||||||
"id_token": idToken,
|
|
||||||
})
|
|
||||||
return token, &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
||||||
Token: token,
|
|
||||||
IDTokenClaims: claims,
|
|
||||||
IDToken: idToken,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
rp := &relyingParty{
|
|
||||||
oauth2Only: tt.oauth2Only,
|
|
||||||
idTokenVerifier: verifier,
|
|
||||||
}
|
|
||||||
token, want := tt.tokens()
|
|
||||||
got, err := verifyTokenResponse[*oidc.IDTokenClaims](context.Background(), token, rp)
|
|
||||||
require.ErrorIs(t, err, tt.wantErr)
|
|
||||||
assert.Equal(t, want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc/grants/tokenexchange"
|
"github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`
|
// TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
package rp_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserInfo struct {
|
|
||||||
Subject string `json:"sub,omitempty"`
|
|
||||||
oidc.UserInfoProfile
|
|
||||||
oidc.UserInfoEmail
|
|
||||||
oidc.UserInfoPhone
|
|
||||||
Address *oidc.UserInfoAddress `json:"address,omitempty"`
|
|
||||||
|
|
||||||
// Foo and Bar are custom claims
|
|
||||||
Foo string `json:"foo,omitempty"`
|
|
||||||
Bar struct {
|
|
||||||
Val1 string `json:"val_1,omitempty"`
|
|
||||||
Val2 string `json:"val_2,omitempty"`
|
|
||||||
} `json:"bar,omitempty"`
|
|
||||||
|
|
||||||
// Claims are all the combined claims, including custom.
|
|
||||||
Claims map[string]any `json:"-,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UserInfo) GetSubject() string {
|
|
||||||
return u.Subject
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleUserinfo_custom() {
|
|
||||||
rpo, err := rp.NewRelyingPartyOIDC(context.TODO(), "http://localhost:8080", "clientid", "clientsecret", "http://example.com/redirect", []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := rp.Userinfo[*UserInfo](context.TODO(), "accesstokenstring", "Bearer", "userid", rpo)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(info)
|
|
||||||
}
|
|
|
@ -4,18 +4,24 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type IDTokenVerifier interface {
|
||||||
|
oidc.Verifier
|
||||||
|
ClientID() string
|
||||||
|
SupportedSignAlgs() []string
|
||||||
|
KeySet() oidc.KeySet
|
||||||
|
Nonce(context.Context) string
|
||||||
|
ACR() oidc.ACRVerifier
|
||||||
|
MaxAge() time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
// VerifyTokens implement the Token Response Validation as defined in OIDC specification
|
// VerifyTokens implement the Token Response Validation as defined in OIDC specification
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
|
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
|
||||||
func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v *IDTokenVerifier) (claims C, err error) {
|
func VerifyTokens[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
|
var nilClaims C
|
||||||
|
|
||||||
claims, err = VerifyIDToken[C](ctx, idToken, v)
|
claims, err = VerifyIDToken[C](ctx, idToken, v)
|
||||||
|
@ -30,10 +36,7 @@ func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken str
|
||||||
|
|
||||||
// VerifyIDToken validates the id token according to
|
// VerifyIDToken validates the id token according to
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||||
func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenVerifier) (claims C, err error) {
|
func VerifyIDToken[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
|
var nilClaims C
|
||||||
|
|
||||||
decrypted, err := oidc.DecryptToken(token)
|
decrypted, err := oidc.DecryptToken(token)
|
||||||
|
@ -49,48 +52,44 @@ func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenV
|
||||||
return nilClaims, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckIssuer(claims, v.Issuer); err != nil {
|
if err = oidc.CheckIssuer(claims, v.Issuer()); err != nil {
|
||||||
return nilClaims, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAudience(claims, v.ClientID); err != nil {
|
if err = oidc.CheckAudience(claims, v.ClientID()); err != nil {
|
||||||
return nilClaims, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAuthorizedParty(claims, v.ClientID); err != nil {
|
if err = oidc.CheckAuthorizedParty(claims, v.ClientID()); err != nil {
|
||||||
return nilClaims, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs, v.KeySet); err != nil {
|
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
|
||||||
return nilClaims, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckExpiration(claims, v.Offset); err != nil {
|
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
|
||||||
return nilClaims, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil {
|
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil {
|
||||||
return nilClaims, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.Nonce != nil {
|
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
|
||||||
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
|
|
||||||
return nilClaims, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil {
|
|
||||||
return nilClaims, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil {
|
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 nilClaims, err
|
||||||
}
|
}
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type IDTokenVerifier oidc.Verifier
|
|
||||||
|
|
||||||
// VerifyAccessToken validates the access token according to
|
// VerifyAccessToken validates the access token according to
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
|
// https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
|
||||||
func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
|
func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
|
||||||
|
@ -108,14 +107,15 @@ func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAl
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIDTokenVerifier returns a oidc.Verifier suitable for ID token verification.
|
// NewIDTokenVerifier returns an implementation of `IDTokenVerifier`
|
||||||
func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) *IDTokenVerifier {
|
// for `VerifyTokens` and `VerifyIDToken`
|
||||||
v := &IDTokenVerifier{
|
func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...VerifierOption) IDTokenVerifier {
|
||||||
Issuer: issuer,
|
v := &idTokenVerifier{
|
||||||
ClientID: clientID,
|
issuer: issuer,
|
||||||
KeySet: keySet,
|
clientID: clientID,
|
||||||
Offset: time.Second,
|
keySet: keySet,
|
||||||
Nonce: func(_ context.Context) string {
|
offset: time.Second,
|
||||||
|
nonce: func(_ context.Context) string {
|
||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -128,47 +128,95 @@ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifierOption is the type for providing dynamic options to the IDTokenVerifier
|
// VerifierOption is the type for providing dynamic options to the IDTokenVerifier
|
||||||
type VerifierOption func(*IDTokenVerifier)
|
type VerifierOption func(*idTokenVerifier)
|
||||||
|
|
||||||
// WithIssuedAtOffset mitigates the risk of iat to be in the future
|
// WithIssuedAtOffset mitigates the risk of iat to be in the future
|
||||||
// because of clock skews with the ability to add an offset to the current time
|
// because of clock skews with the ability to add an offset to the current time
|
||||||
func WithIssuedAtOffset(offset time.Duration) VerifierOption {
|
func WithIssuedAtOffset(offset time.Duration) func(*idTokenVerifier) {
|
||||||
return func(v *IDTokenVerifier) {
|
return func(v *idTokenVerifier) {
|
||||||
v.Offset = offset
|
v.offset = offset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
|
// WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
|
||||||
func WithIssuedAtMaxAge(maxAge time.Duration) VerifierOption {
|
func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) {
|
||||||
return func(v *IDTokenVerifier) {
|
return func(v *idTokenVerifier) {
|
||||||
v.MaxAgeIAT = maxAge
|
v.maxAgeIAT = maxAge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithNonce sets the function to check the nonce
|
// WithNonce sets the function to check the nonce
|
||||||
func WithNonce(nonce func(context.Context) string) VerifierOption {
|
func WithNonce(nonce func(context.Context) string) VerifierOption {
|
||||||
return func(v *IDTokenVerifier) {
|
return func(v *idTokenVerifier) {
|
||||||
v.Nonce = nonce
|
v.nonce = nonce
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithACRVerifier sets the verifier for the acr claim
|
// WithACRVerifier sets the verifier for the acr claim
|
||||||
func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption {
|
func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption {
|
||||||
return func(v *IDTokenVerifier) {
|
return func(v *idTokenVerifier) {
|
||||||
v.ACR = verifier
|
v.acr = verifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now
|
// WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now
|
||||||
func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption {
|
func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption {
|
||||||
return func(v *IDTokenVerifier) {
|
return func(v *idTokenVerifier) {
|
||||||
v.MaxAge = maxAge
|
v.maxAge = maxAge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm
|
// WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm
|
||||||
func WithSupportedSigningAlgorithms(algs ...string) VerifierOption {
|
func WithSupportedSigningAlgorithms(algs ...string) VerifierOption {
|
||||||
return func(v *IDTokenVerifier) {
|
return func(v *idTokenVerifier) {
|
||||||
v.SupportedSignAlgs = algs
|
v.supportedSignAlgs = algs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type idTokenVerifier struct {
|
||||||
|
issuer string
|
||||||
|
maxAgeIAT time.Duration
|
||||||
|
offset time.Duration
|
||||||
|
clientID string
|
||||||
|
supportedSignAlgs []string
|
||||||
|
keySet oidc.KeySet
|
||||||
|
acr oidc.ACRVerifier
|
||||||
|
maxAge time.Duration
|
||||||
|
nonce func(ctx context.Context) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idTokenVerifier) Issuer() string {
|
||||||
|
return i.issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idTokenVerifier) MaxAgeIAT() time.Duration {
|
||||||
|
return i.maxAgeIAT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idTokenVerifier) Offset() time.Duration {
|
||||||
|
return i.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idTokenVerifier) ClientID() string {
|
||||||
|
return i.clientID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idTokenVerifier) SupportedSignAlgs() []string {
|
||||||
|
return i.supportedSignAlgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idTokenVerifier) KeySet() oidc.KeySet {
|
||||||
|
return i.keySet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idTokenVerifier) Nonce(ctx context.Context) string {
|
||||||
|
return i.nonce(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idTokenVerifier) ACR() oidc.ACRVerifier {
|
||||||
|
return i.acr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idTokenVerifier) MaxAge() time.Duration {
|
||||||
|
return i.maxAge
|
||||||
|
}
|
||||||
|
|
|
@ -5,24 +5,24 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestVerifyTokens(t *testing.T) {
|
func TestVerifyTokens(t *testing.T) {
|
||||||
verifier := &IDTokenVerifier{
|
verifier := &idTokenVerifier{
|
||||||
Issuer: tu.ValidIssuer,
|
issuer: tu.ValidIssuer,
|
||||||
MaxAgeIAT: 2 * time.Minute,
|
maxAgeIAT: 2 * time.Minute,
|
||||||
Offset: time.Second,
|
offset: time.Second,
|
||||||
SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||||
KeySet: tu.KeySet{},
|
keySet: tu.KeySet{},
|
||||||
MaxAge: 2 * time.Minute,
|
maxAge: 2 * time.Minute,
|
||||||
ACR: tu.ACRVerify,
|
acr: tu.ACRVerify,
|
||||||
Nonce: func(context.Context) string { return tu.ValidNonce },
|
nonce: func(context.Context) string { return tu.ValidNonce },
|
||||||
ClientID: tu.ValidClientID,
|
clientID: tu.ValidClientID,
|
||||||
}
|
}
|
||||||
accessToken, _ := tu.ValidAccessToken()
|
accessToken, _ := tu.ValidAccessToken()
|
||||||
atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm)
|
atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm)
|
||||||
|
@ -91,64 +91,43 @@ func TestVerifyTokens(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVerifyIDToken(t *testing.T) {
|
func TestVerifyIDToken(t *testing.T) {
|
||||||
verifier := &IDTokenVerifier{
|
verifier := &idTokenVerifier{
|
||||||
Issuer: tu.ValidIssuer,
|
issuer: tu.ValidIssuer,
|
||||||
MaxAgeIAT: 2 * time.Minute,
|
maxAgeIAT: 2 * time.Minute,
|
||||||
Offset: time.Second,
|
offset: time.Second,
|
||||||
SupportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||||
KeySet: tu.KeySet{},
|
keySet: tu.KeySet{},
|
||||||
MaxAge: 2 * time.Minute,
|
maxAge: 2 * time.Minute,
|
||||||
ACR: tu.ACRVerify,
|
acr: tu.ACRVerify,
|
||||||
Nonce: func(context.Context) string { return tu.ValidNonce },
|
nonce: func(context.Context) string { return tu.ValidNonce },
|
||||||
ClientID: tu.ValidClientID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
tokenClaims func() (string, *oidc.IDTokenClaims)
|
clientID string
|
||||||
customVerifier func(verifier *IDTokenVerifier)
|
tokenClaims func() (string, *oidc.IDTokenClaims)
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "success",
|
name: "success",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
tokenClaims: tu.ValidIDToken,
|
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",
|
name: "parse err",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil },
|
tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil },
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid signature",
|
name: "invalid signature",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil },
|
tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil },
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty subject",
|
name: "empty subject",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
return tu.NewIDToken(
|
return tu.NewIDToken(
|
||||||
tu.ValidIssuer, "", tu.ValidAudience,
|
tu.ValidIssuer, "", tu.ValidAudience,
|
||||||
|
@ -159,7 +138,8 @@ func TestVerifyIDToken(t *testing.T) {
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong issuer",
|
name: "wrong issuer",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
return tu.NewIDToken(
|
return tu.NewIDToken(
|
||||||
"foo", tu.ValidSubject, tu.ValidAudience,
|
"foo", tu.ValidSubject, tu.ValidAudience,
|
||||||
|
@ -170,15 +150,14 @@ func TestVerifyIDToken(t *testing.T) {
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong clientID",
|
name: "wrong clientID",
|
||||||
customVerifier: func(verifier *IDTokenVerifier) {
|
clientID: "foo",
|
||||||
verifier.ClientID = "foo"
|
|
||||||
},
|
|
||||||
tokenClaims: tu.ValidIDToken,
|
tokenClaims: tu.ValidIDToken,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "expired",
|
name: "expired",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
return tu.NewIDToken(
|
return tu.NewIDToken(
|
||||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
@ -189,7 +168,8 @@ func TestVerifyIDToken(t *testing.T) {
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong IAT",
|
name: "wrong IAT",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
return tu.NewIDToken(
|
return tu.NewIDToken(
|
||||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
@ -200,7 +180,8 @@ func TestVerifyIDToken(t *testing.T) {
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong acr",
|
name: "wrong acr",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
return tu.NewIDToken(
|
return tu.NewIDToken(
|
||||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
@ -211,7 +192,8 @@ func TestVerifyIDToken(t *testing.T) {
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "expired auth",
|
name: "expired auth",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
return tu.NewIDToken(
|
return tu.NewIDToken(
|
||||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
@ -222,7 +204,8 @@ func TestVerifyIDToken(t *testing.T) {
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong nonce",
|
name: "wrong nonce",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
return tu.NewIDToken(
|
return tu.NewIDToken(
|
||||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
@ -236,10 +219,7 @@ func TestVerifyIDToken(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
token, want := tt.tokenClaims()
|
token, want := tt.tokenClaims()
|
||||||
if tt.customVerifier != nil {
|
verifier.clientID = tt.clientID
|
||||||
tt.customVerifier(verifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier)
|
got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
@ -320,7 +300,7 @@ func TestNewIDTokenVerifier(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
want *IDTokenVerifier
|
want IDTokenVerifier
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "nil nonce", // otherwise assert.Equal will fail on the function
|
name: "nil nonce", // otherwise assert.Equal will fail on the function
|
||||||
|
@ -337,16 +317,16 @@ func TestNewIDTokenVerifier(t *testing.T) {
|
||||||
WithSupportedSigningAlgorithms("ABC", "DEF"),
|
WithSupportedSigningAlgorithms("ABC", "DEF"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &IDTokenVerifier{
|
want: &idTokenVerifier{
|
||||||
Issuer: tu.ValidIssuer,
|
issuer: tu.ValidIssuer,
|
||||||
Offset: time.Minute,
|
offset: time.Minute,
|
||||||
MaxAgeIAT: time.Hour,
|
maxAgeIAT: time.Hour,
|
||||||
ClientID: tu.ValidClientID,
|
clientID: tu.ValidClientID,
|
||||||
KeySet: tu.KeySet{},
|
keySet: tu.KeySet{},
|
||||||
Nonce: nil,
|
nonce: nil,
|
||||||
ACR: nil,
|
acr: nil,
|
||||||
MaxAge: 2 * time.Hour,
|
maxAge: 2 * time.Hour,
|
||||||
SupportedSignAlgs: []string{"ABC", "DEF"},
|
supportedSignAlgs: []string{"ABC", "DEF"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MyCustomClaims extends the TokenClaims base,
|
// MyCustomClaims extends the TokenClaims base,
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
package rs_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IntrospectionResponse struct {
|
|
||||||
Active bool `json:"active"`
|
|
||||||
Scope oidc.SpaceDelimitedArray `json:"scope,omitempty"`
|
|
||||||
ClientID string `json:"client_id,omitempty"`
|
|
||||||
TokenType string `json:"token_type,omitempty"`
|
|
||||||
Expiration oidc.Time `json:"exp,omitempty"`
|
|
||||||
IssuedAt oidc.Time `json:"iat,omitempty"`
|
|
||||||
NotBefore oidc.Time `json:"nbf,omitempty"`
|
|
||||||
Subject string `json:"sub,omitempty"`
|
|
||||||
Audience oidc.Audience `json:"aud,omitempty"`
|
|
||||||
Issuer string `json:"iss,omitempty"`
|
|
||||||
JWTID string `json:"jti,omitempty"`
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
oidc.UserInfoProfile
|
|
||||||
oidc.UserInfoEmail
|
|
||||||
oidc.UserInfoPhone
|
|
||||||
Address *oidc.UserInfoAddress `json:"address,omitempty"`
|
|
||||||
|
|
||||||
// Foo and Bar are custom claims
|
|
||||||
Foo string `json:"foo,omitempty"`
|
|
||||||
Bar struct {
|
|
||||||
Val1 string `json:"val_1,omitempty"`
|
|
||||||
Val2 string `json:"val_2,omitempty"`
|
|
||||||
} `json:"bar,omitempty"`
|
|
||||||
|
|
||||||
// Claims are all the combined claims, including custom.
|
|
||||||
Claims map[string]any `json:"-,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleIntrospect_custom() {
|
|
||||||
rss, err := rs.NewResourceServerClientCredentials(context.TODO(), "http://localhost:8080", "clientid", "clientsecret")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := rs.Introspect[*IntrospectionResponse](context.TODO(), rss, "accesstokenstring")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(resp)
|
|
||||||
}
|
|
|
@ -6,16 +6,16 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
"github.com/zitadel/oidc/v2/pkg/client"
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceServer interface {
|
type ResourceServer interface {
|
||||||
IntrospectionURL() string
|
IntrospectionURL() string
|
||||||
TokenEndpoint() string
|
TokenEndpoint() string
|
||||||
HttpClient() *http.Client
|
HttpClient() *http.Client
|
||||||
AuthFn() (any, error)
|
AuthFn() (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type resourceServer struct {
|
type resourceServer struct {
|
||||||
|
@ -23,7 +23,7 @@ type resourceServer struct {
|
||||||
tokenURL string
|
tokenURL string
|
||||||
introspectURL string
|
introspectURL string
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
authFn func() (any, error)
|
authFn func() (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resourceServer) IntrospectionURL() string {
|
func (r *resourceServer) IntrospectionURL() string {
|
||||||
|
@ -38,33 +38,33 @@ func (r *resourceServer) HttpClient() *http.Client {
|
||||||
return r.httpClient
|
return r.httpClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resourceServer) AuthFn() (any, error) {
|
func (r *resourceServer) AuthFn() (interface{}, error) {
|
||||||
return r.authFn()
|
return r.authFn()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResourceServerClientCredentials(ctx context.Context, issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) {
|
func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option ...Option) (ResourceServer, error) {
|
||||||
authorizer := func() (any, error) {
|
authorizer := func() (interface{}, error) {
|
||||||
return httphelper.AuthorizeBasic(clientID, clientSecret), nil
|
return httphelper.AuthorizeBasic(clientID, clientSecret), nil
|
||||||
}
|
}
|
||||||
return newResourceServer(ctx, issuer, authorizer, option...)
|
return newResourceServer(issuer, authorizer, option...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResourceServerJWTProfile(ctx context.Context, issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) {
|
func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) {
|
||||||
signer, err := client.NewSignerFromPrivateKeyByte(key, keyID)
|
signer, err := client.NewSignerFromPrivateKeyByte(key, keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
authorizer := func() (any, error) {
|
authorizer := func() (interface{}, error) {
|
||||||
assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer)
|
assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return client.ClientAssertionFormAuthorization(assertion), nil
|
return client.ClientAssertionFormAuthorization(assertion), nil
|
||||||
}
|
}
|
||||||
return newResourceServer(ctx, issuer, authorizer, options...)
|
return newResourceServer(issuer, authorizer, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newResourceServer(ctx context.Context, issuer string, authorizer func() (any, error), options ...Option) (*resourceServer, error) {
|
func newResourceServer(issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) {
|
||||||
rs := &resourceServer{
|
rs := &resourceServer{
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
httpClient: httphelper.DefaultHTTPClient,
|
httpClient: httphelper.DefaultHTTPClient,
|
||||||
|
@ -73,30 +73,26 @@ func newResourceServer(ctx context.Context, issuer string, authorizer func() (an
|
||||||
optFunc(rs)
|
optFunc(rs)
|
||||||
}
|
}
|
||||||
if rs.introspectURL == "" || rs.tokenURL == "" {
|
if rs.introspectURL == "" || rs.tokenURL == "" {
|
||||||
config, err := client.Discover(ctx, rs.issuer, rs.httpClient)
|
config, err := client.Discover(rs.issuer, rs.httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if rs.tokenURL == "" {
|
rs.tokenURL = config.TokenEndpoint
|
||||||
rs.tokenURL = config.TokenEndpoint
|
rs.introspectURL = config.IntrospectionEndpoint
|
||||||
}
|
|
||||||
if rs.introspectURL == "" {
|
|
||||||
rs.introspectURL = config.IntrospectionEndpoint
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if rs.tokenURL == "" {
|
if rs.introspectURL == "" || rs.tokenURL == "" {
|
||||||
return nil, errors.New("tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url")
|
return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url")
|
||||||
}
|
}
|
||||||
rs.authFn = authorizer
|
rs.authFn = authorizer
|
||||||
return rs, nil
|
return rs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResourceServerFromKeyFile(ctx context.Context, issuer, path string, options ...Option) (ResourceServer, error) {
|
func NewResourceServerFromKeyFile(issuer, path string, options ...Option) (ResourceServer, error) {
|
||||||
c, err := client.ConfigFromKeyFile(path)
|
c, err := client.ConfigFromKeyFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewResourceServerJWTProfile(ctx, issuer, c.ClientID, c.KeyID, []byte(c.Key), options...)
|
return NewResourceServerJWTProfile(issuer, c.ClientID, c.KeyID, []byte(c.Key), options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option func(*resourceServer)
|
type Option func(*resourceServer)
|
||||||
|
@ -116,30 +112,18 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Introspect calls the [RFC7662] Token Introspection
|
func Introspect(ctx context.Context, rp ResourceServer, token string) (*oidc.IntrospectionResponse, error) {
|
||||||
// endpoint and returns the response in an instance of type R.
|
|
||||||
// [*oidc.IntrospectionResponse] can be used as a good example, or use a custom type if type-safe
|
|
||||||
// access to custom claims is needed.
|
|
||||||
//
|
|
||||||
// [RFC7662]: https://www.rfc-editor.org/rfc/rfc7662
|
|
||||||
func Introspect[R any](ctx context.Context, rp ResourceServer, token string) (resp R, err error) {
|
|
||||||
ctx, span := client.Tracer.Start(ctx, "Introspect")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
if rp.IntrospectionURL() == "" {
|
|
||||||
return resp, errors.New("resource server: introspection URL is empty")
|
|
||||||
}
|
|
||||||
authFn, err := rp.AuthFn()
|
authFn, err := rp.AuthFn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req, err := httphelper.FormRequest(ctx, rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn)
|
req, err := httphelper.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
resp := new(oidc.IntrospectionResponse)
|
||||||
if err := httphelper.HttpRequest(rp.HttpClient(), req, &resp); err != nil {
|
if err := httphelper.HttpRequest(rp.HttpClient(), req, resp); err != nil {
|
||||||
return resp, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,221 +0,0 @@
|
||||||
package rs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewResourceServer(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
issuer string
|
|
||||||
authorizer func() (any, error)
|
|
||||||
options []Option
|
|
||||||
}
|
|
||||||
type wantFields struct {
|
|
||||||
issuer string
|
|
||||||
tokenURL string
|
|
||||||
introspectURL string
|
|
||||||
authFn func() (any, error)
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantFields *wantFields
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "spotify-full-discovery",
|
|
||||||
args: args{
|
|
||||||
issuer: "https://accounts.spotify.com",
|
|
||||||
authorizer: nil,
|
|
||||||
options: []Option{},
|
|
||||||
},
|
|
||||||
wantFields: &wantFields{
|
|
||||||
issuer: "https://accounts.spotify.com",
|
|
||||||
tokenURL: "https://accounts.spotify.com/api/token",
|
|
||||||
introspectURL: "",
|
|
||||||
authFn: nil,
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "spotify-with-static-tokenurl",
|
|
||||||
args: args{
|
|
||||||
issuer: "https://accounts.spotify.com",
|
|
||||||
authorizer: nil,
|
|
||||||
options: []Option{
|
|
||||||
WithStaticEndpoints(
|
|
||||||
"https://some.host/token-url",
|
|
||||||
"",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantFields: &wantFields{
|
|
||||||
issuer: "https://accounts.spotify.com",
|
|
||||||
tokenURL: "https://some.host/token-url",
|
|
||||||
introspectURL: "",
|
|
||||||
authFn: nil,
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "spotify-with-static-introspecturl",
|
|
||||||
args: args{
|
|
||||||
issuer: "https://accounts.spotify.com",
|
|
||||||
authorizer: nil,
|
|
||||||
options: []Option{
|
|
||||||
WithStaticEndpoints(
|
|
||||||
"",
|
|
||||||
"https://some.host/instrospect-url",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantFields: &wantFields{
|
|
||||||
issuer: "https://accounts.spotify.com",
|
|
||||||
tokenURL: "https://accounts.spotify.com/api/token",
|
|
||||||
introspectURL: "https://some.host/instrospect-url",
|
|
||||||
authFn: nil,
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "spotify-with-all-static-endpoints",
|
|
||||||
args: args{
|
|
||||||
issuer: "https://accounts.spotify.com",
|
|
||||||
authorizer: nil,
|
|
||||||
options: []Option{
|
|
||||||
WithStaticEndpoints(
|
|
||||||
"https://some.host/token-url",
|
|
||||||
"https://some.host/instrospect-url",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantFields: &wantFields{
|
|
||||||
issuer: "https://accounts.spotify.com",
|
|
||||||
tokenURL: "https://some.host/token-url",
|
|
||||||
introspectURL: "https://some.host/instrospect-url",
|
|
||||||
authFn: nil,
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad-discovery",
|
|
||||||
args: args{
|
|
||||||
issuer: "https://127.0.0.1:65535",
|
|
||||||
authorizer: nil,
|
|
||||||
options: []Option{},
|
|
||||||
},
|
|
||||||
wantFields: nil,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad-discovery-with-static-tokenurl",
|
|
||||||
args: args{
|
|
||||||
issuer: "https://127.0.0.1:65535",
|
|
||||||
authorizer: nil,
|
|
||||||
options: []Option{
|
|
||||||
WithStaticEndpoints(
|
|
||||||
"https://some.host/token-url",
|
|
||||||
"",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantFields: nil,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad-discovery-with-static-introspecturl",
|
|
||||||
args: args{
|
|
||||||
issuer: "https://127.0.0.1:65535",
|
|
||||||
authorizer: nil,
|
|
||||||
options: []Option{
|
|
||||||
WithStaticEndpoints(
|
|
||||||
"",
|
|
||||||
"https://some.host/instrospect-url",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantFields: nil,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad-discovery-with-all-static-endpoints",
|
|
||||||
args: args{
|
|
||||||
issuer: "https://127.0.0.1:65535",
|
|
||||||
authorizer: nil,
|
|
||||||
options: []Option{
|
|
||||||
WithStaticEndpoints(
|
|
||||||
"https://some.host/token-url",
|
|
||||||
"https://some.host/instrospect-url",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantFields: &wantFields{
|
|
||||||
issuer: "https://127.0.0.1:65535",
|
|
||||||
tokenURL: "https://some.host/token-url",
|
|
||||||
introspectURL: "https://some.host/instrospect-url",
|
|
||||||
authFn: nil,
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := newResourceServer(context.Background(), tt.args.issuer, tt.args.authorizer, tt.args.options...)
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
if tt.wantFields == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.Equal(t, tt.wantFields.issuer, got.issuer)
|
|
||||||
assert.Equal(t, tt.wantFields.tokenURL, got.tokenURL)
|
|
||||||
assert.Equal(t, tt.wantFields.introspectURL, got.introspectURL)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntrospect(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
ctx context.Context
|
|
||||||
rp ResourceServer
|
|
||||||
token string
|
|
||||||
}
|
|
||||||
rp, err := newResourceServer(
|
|
||||||
context.Background(),
|
|
||||||
"https://accounts.spotify.com",
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "missing-introspect-url",
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
rp: rp,
|
|
||||||
token: "my-token",
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
_, err := Introspect[*oidc.IntrospectionResponse](tt.args.ctx, tt.args.rp, tt.args.token)
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +1,38 @@
|
||||||
package tokenexchange
|
package tokenexchange
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
"github.com/zitadel/oidc/v2/pkg/client"
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
"github.com/go-jose/go-jose/v4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TokenExchanger interface {
|
type TokenExchanger interface {
|
||||||
TokenEndpoint() string
|
TokenEndpoint() string
|
||||||
HttpClient() *http.Client
|
HttpClient() *http.Client
|
||||||
AuthFn() (any, error)
|
AuthFn() (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthTokenExchange struct {
|
type OAuthTokenExchange struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
tokenEndpoint string
|
tokenEndpoint string
|
||||||
authFn func() (any, error)
|
authFn func() (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTokenExchanger(ctx context.Context, issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
|
func NewTokenExchanger(issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
|
||||||
return newOAuthTokenExchange(ctx, issuer, nil, options...)
|
return newOAuthTokenExchange(issuer, nil, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTokenExchangerClientCredentials(ctx context.Context, issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
|
func NewTokenExchangerClientCredentials(issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
|
||||||
authorizer := func() (any, error) {
|
authorizer := func() (interface{}, error) {
|
||||||
return httphelper.AuthorizeBasic(clientID, clientSecret), nil
|
return httphelper.AuthorizeBasic(clientID, clientSecret), nil
|
||||||
}
|
}
|
||||||
return newOAuthTokenExchange(ctx, issuer, authorizer, options...)
|
return newOAuthTokenExchange(issuer, authorizer, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTokenExchangerJWTProfile(ctx context.Context, issuer, clientID string, signer jose.Signer, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
|
func newOAuthTokenExchange(issuer string, authorizer func() (interface{}, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) {
|
||||||
authorizer := func() (any, error) {
|
|
||||||
assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client.ClientAssertionFormAuthorization(assertion), nil
|
|
||||||
}
|
|
||||||
return newOAuthTokenExchange(ctx, issuer, authorizer, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOAuthTokenExchange(ctx context.Context, issuer string, authorizer func() (any, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) {
|
|
||||||
te := &OAuthTokenExchange{
|
te := &OAuthTokenExchange{
|
||||||
httpClient: httphelper.DefaultHTTPClient,
|
httpClient: httphelper.DefaultHTTPClient,
|
||||||
}
|
}
|
||||||
|
@ -55,7 +41,7 @@ func newOAuthTokenExchange(ctx context.Context, issuer string, authorizer func()
|
||||||
}
|
}
|
||||||
|
|
||||||
if te.tokenEndpoint == "" {
|
if te.tokenEndpoint == "" {
|
||||||
config, err := client.Discover(ctx, issuer, te.httpClient)
|
config, err := client.Discover(issuer, te.httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -92,7 +78,7 @@ func (te *OAuthTokenExchange) HttpClient() *http.Client {
|
||||||
return te.httpClient
|
return te.httpClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (te *OAuthTokenExchange) AuthFn() (any, error) {
|
func (te *OAuthTokenExchange) AuthFn() (interface{}, error) {
|
||||||
if te.authFn != nil {
|
if te.authFn != nil {
|
||||||
return te.authFn()
|
return te.authFn()
|
||||||
}
|
}
|
||||||
|
@ -103,7 +89,6 @@ func (te *OAuthTokenExchange) AuthFn() (any, error) {
|
||||||
// ExchangeToken sends a token exchange request (rfc 8693) to te's token endpoint.
|
// ExchangeToken sends a token exchange request (rfc 8693) to te's token endpoint.
|
||||||
// SubjectToken and SubjectTokenType are required parameters.
|
// SubjectToken and SubjectTokenType are required parameters.
|
||||||
func ExchangeToken(
|
func ExchangeToken(
|
||||||
ctx context.Context,
|
|
||||||
te TokenExchanger,
|
te TokenExchanger,
|
||||||
SubjectToken string,
|
SubjectToken string,
|
||||||
SubjectTokenType oidc.TokenType,
|
SubjectTokenType oidc.TokenType,
|
||||||
|
@ -114,9 +99,6 @@ func ExchangeToken(
|
||||||
Scopes []string,
|
Scopes []string,
|
||||||
RequestedTokenType oidc.TokenType,
|
RequestedTokenType oidc.TokenType,
|
||||||
) (*oidc.TokenExchangeResponse, error) {
|
) (*oidc.TokenExchangeResponse, error) {
|
||||||
ctx, span := client.Tracer.Start(ctx, "ExchangeToken")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
if SubjectToken == "" {
|
if SubjectToken == "" {
|
||||||
return nil, errors.New("empty subject_token")
|
return nil, errors.New("empty subject_token")
|
||||||
}
|
}
|
||||||
|
@ -141,5 +123,5 @@ func ExchangeToken(
|
||||||
RequestedTokenType: RequestedTokenType,
|
RequestedTokenType: RequestedTokenType,
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.CallTokenExchangeEndpoint(ctx, request, authFn, te)
|
return client.CallTokenExchangeEndpoint(request, authFn, te)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm")
|
var ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm")
|
||||||
|
@ -21,14 +21,6 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) {
|
||||||
return sha512.New384(), nil
|
return sha512.New384(), nil
|
||||||
case jose.RS512, jose.ES512, jose.PS512:
|
case jose.RS512, jose.ES512, jose.PS512:
|
||||||
return sha512.New(), nil
|
return sha512.New(), nil
|
||||||
|
|
||||||
// There is no published spec for this yet, but we have confirmation it will get published.
|
|
||||||
// There is consensus here: https://bitbucket.org/openid/connect/issues/1125/_hash-algorithm-for-eddsa-id-tokens
|
|
||||||
// Currently Go and go-jose only supports the ed25519 curve key for EdDSA, so we can safely assume sha512 here.
|
|
||||||
// It is unlikely ed448 will ever be supported: https://github.com/golang/go/issues/29390
|
|
||||||
case jose.EdDSA:
|
|
||||||
return sha512.New(), nil
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm)
|
return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,17 @@
|
||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/ed25519"
|
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/go-jose/go-jose/v4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
|
||||||
ErrPEMDecode = errors.New("PEM decode failed")
|
block, _ := pem.Decode(priv)
|
||||||
ErrUnsupportedFormat = errors.New("key is neither in PKCS#1 nor PKCS#8 format")
|
b := block.Bytes
|
||||||
ErrUnsupportedPrivateKey = errors.New("unsupported key type, must be RSA, ECDSA or ED25519 private key")
|
key, err := x509.ParsePKCS1PrivateKey(b)
|
||||||
)
|
|
||||||
|
|
||||||
func BytesToPrivateKey(b []byte) (crypto.PublicKey, jose.SignatureAlgorithm, error) {
|
|
||||||
block, _ := pem.Decode(b)
|
|
||||||
if block == nil {
|
|
||||||
return nil, "", ErrPEMDecode
|
|
||||||
}
|
|
||||||
|
|
||||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
||||||
if err == nil {
|
|
||||||
return privateKey, jose.RS256, nil
|
|
||||||
}
|
|
||||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", ErrUnsupportedFormat
|
return nil, err
|
||||||
}
|
|
||||||
switch privateKey := key.(type) {
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
return privateKey, jose.RS256, nil
|
|
||||||
case ed25519.PrivateKey:
|
|
||||||
return privateKey, jose.EdDSA, nil
|
|
||||||
case *ecdsa.PrivateKey:
|
|
||||||
return privateKey, jose.ES256, nil
|
|
||||||
default:
|
|
||||||
return nil, "", ErrUnsupportedPrivateKey
|
|
||||||
}
|
}
|
||||||
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,134 +0,0 @@
|
||||||
package crypto_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/ed25519"
|
|
||||||
"crypto/rsa"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/go-jose/go-jose/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
zcrypto "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBytesToPrivateKey(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
key []byte
|
|
||||||
}
|
|
||||||
type want struct {
|
|
||||||
key crypto.Signer
|
|
||||||
algorithm jose.SignatureAlgorithm
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want want
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "PEMDecodeError",
|
|
||||||
args: args{
|
|
||||||
key: []byte("The non-PEM sequence"),
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
err: zcrypto.ErrPEMDecode,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "PKCS#1 RSA",
|
|
||||||
args: args{
|
|
||||||
key: []byte(`-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
|
|
||||||
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
|
|
||||||
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
|
|
||||||
TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7
|
|
||||||
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
|
|
||||||
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
|
|
||||||
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
|
|
||||||
-----END RSA PRIVATE KEY-----`),
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
key: &rsa.PrivateKey{},
|
|
||||||
algorithm: jose.RS256,
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "PKCS#8 RSA",
|
|
||||||
args: args{
|
|
||||||
key: []byte(`-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfaDB7pK/fmP/I
|
|
||||||
7IusSK8lTCBnPZghqIbVLt2QHYAMoEF1CaF4F4rxo2vl1Mt8gwsq4T3osQFZMvnL
|
|
||||||
YHb7KNyUoJgTjLxJQADv2u4Q3U38heAzK5Tp4ry4MCnuyJIqAPK1GiruwEq4zQrx
|
|
||||||
+WzVix8otO37SuW9tzklqlNGMiAYBL0TBKHvS5XMbjP1idBMB8erMz29w/TVQnEB
|
|
||||||
Kj0vCdZjrbVPKygptt5kcSrL5f4xCZwU+ufz7cp0GLwpRMJ+shG9YJJFBxb0itPF
|
|
||||||
sy51vAyEtdBC7jgAU96ZVeQ06nryDq1D2EpoVMElqNyL46Jo3lnKbGquGKzXzQYU
|
|
||||||
BN32/scDAgMBAAECggEBAJE/mo3PLgILo2YtQ8ekIxNVHmF0Gl7w9IrjvTdH6hmX
|
|
||||||
HI3MTLjkmtI7GmG9V/0IWvCjdInGX3grnrjWGRQZ04QKIQgPQLFuBGyJjEsJm7nx
|
|
||||||
MqztlS7YTyV1nX/aenSTkJO8WEpcJLnm+4YoxCaAMdAhrIdBY71OamALpv1bRysa
|
|
||||||
FaiCGcemT2yqZn0GqIS8O26Tz5zIqrTN2G1eSmgh7DG+7FoddMz35cute8R10xUG
|
|
||||||
hF5YU+6fcXiRQ/Kh7nlxelPGqdZFPMk7LpVHzkQKwdJ+N0P23lPDIfNsvpG1n0OP
|
|
||||||
3g5km7gHSrSU2yZ3eFl6DB9x1IFNS9BaQQuSxYJtKwECgYEA1C8jjzpXZDLvlYsV
|
|
||||||
2jlMzkrbsIrX2dzblVrNsPs2jRbjYU8mg2DUDO6lOhtxHfqZG6sO+gmWi/zvoy9l
|
|
||||||
yolGbXe1Jqx66p9fznIcecSwar8+ACa356Wk74Nt1PlBOfCMqaJnYLOLaFJa29Vy
|
|
||||||
u5ClZVzKd5AVXl7yFVd4XfLv/WECgYEAwFMMtFoasdF92c0d31rZ1uoPOtFz6xq6
|
|
||||||
uQggdm5zzkhnfwUAGqppS/u1CHcJ7T/74++jLbFTsaohGr4jEzWSGvJpomEUChy3
|
|
||||||
r25YofMclUhJ5pCEStsLtqiCR1Am6LlI8HMdBEP1QDgEC5q8bQW4+UHuew1E1zxz
|
|
||||||
osZOhe09WuMCgYEA0G9aFCnwjUqIFjQiDFP7gi8BLqTFs4uE3Wvs4W11whV42i+B
|
|
||||||
ms90nxuTjchFT3jMDOT1+mOO0wdudLRr3xEI8SIF/u6ydGaJG+j21huEXehtxIJE
|
|
||||||
aDdNFcfbDbqo+3y1ATK7MMBPMvSrsoY0hdJq127WqasNgr3sO1DIuima3SECgYEA
|
|
||||||
nkM5TyhekzlbIOHD1UsDu/D7+2DkzPE/+oePfyXBMl0unb3VqhvVbmuBO6gJiSx/
|
|
||||||
8b//PdiQkMD5YPJaFrKcuoQFHVRZk0CyfzCEyzAts0K7XXpLAvZiGztriZeRjSz7
|
|
||||||
srJnjF0H8oKmAY6hw+1Tm/n/b08p+RyL48TgVSE2vhUCgYA3BWpkD4PlCcn/FZsq
|
|
||||||
OrLFyFXI6jIaxskFtsRW1IxxIlAdZmxfB26P/2gx6VjLdxJI/RRPkJyEN2dP7CbR
|
|
||||||
BDjb565dy1O9D6+UrY70Iuwjz+OcALRBBGTaiF2pLn6IhSzNI2sy/tXX8q8dBlg9
|
|
||||||
OFCrqT/emes3KytTPfa5NZtYeQ==
|
|
||||||
-----END PRIVATE KEY-----`),
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
key: &rsa.PrivateKey{},
|
|
||||||
algorithm: jose.RS256,
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "PKCS#8 ECDSA",
|
|
||||||
args: args{
|
|
||||||
key: []byte(`-----BEGIN PRIVATE KEY-----
|
|
||||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwwOZSU4GlP7ps/Wp
|
|
||||||
V6o0qRwxultdfYo/uUuj48QZjSuhRANCAATMiI2Han+ABKmrk5CNlxRAGC61w4d3
|
|
||||||
G4TAeuBpyzqJ7x/6NjCxoQzJzZHtNjIfjVATI59XFZWF59GhtSZbShAr
|
|
||||||
-----END PRIVATE KEY-----`),
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
key: &ecdsa.PrivateKey{},
|
|
||||||
algorithm: jose.ES256,
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "PKCS#8 ED25519",
|
|
||||||
args: args{
|
|
||||||
key: []byte(`-----BEGIN PRIVATE KEY-----
|
|
||||||
MC4CAQAwBQYDK2VwBCIEIHu6ZtDsjjauMasBxnS9Fg87UJwKfcT/oiq6S0ktbky8
|
|
||||||
-----END PRIVATE KEY-----`),
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
key: ed25519.PrivateKey{},
|
|
||||||
algorithm: jose.EdDSA,
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
key, algorithm, err := zcrypto.BytesToPrivateKey(tt.args.key)
|
|
||||||
assert.IsType(t, tt.want.key, key)
|
|
||||||
assert.Equal(t, tt.want.algorithm, algorithm)
|
|
||||||
assert.ErrorIs(t, tt.want.err, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,10 +4,10 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Sign(object any, signer jose.Signer) (string, error) {
|
func Sign(object interface{}, signer jose.Signer) (string, error) {
|
||||||
payload, err := json.Marshal(object)
|
payload, err := json.Marshal(object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
@ -10,8 +10,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var DefaultHTTPClient = &http.Client{
|
var DefaultHTTPClient = &http.Client{
|
||||||
|
@ -19,11 +17,11 @@ var DefaultHTTPClient = &http.Client{
|
||||||
}
|
}
|
||||||
|
|
||||||
type Decoder interface {
|
type Decoder interface {
|
||||||
Decode(dst any, src map[string][]string) error
|
Decode(dst interface{}, src map[string][]string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Encoder interface {
|
type Encoder interface {
|
||||||
Encode(src any, dst map[string][]string) error
|
Encode(src interface{}, dst map[string][]string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormAuthorization func(url.Values)
|
type FormAuthorization func(url.Values)
|
||||||
|
@ -35,7 +33,7 @@ func AuthorizeBasic(user, password string) RequestAuthorization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FormRequest(ctx context.Context, endpoint string, request any, encoder Encoder, authFn any) (*http.Request, error) {
|
func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn interface{}) (*http.Request, error) {
|
||||||
form := url.Values{}
|
form := url.Values{}
|
||||||
if err := encoder.Encode(request, form); err != nil {
|
if err := encoder.Encode(request, form); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -44,7 +42,7 @@ func FormRequest(ctx context.Context, endpoint string, request any, encoder Enco
|
||||||
fn(form)
|
fn(form)
|
||||||
}
|
}
|
||||||
body := strings.NewReader(form.Encode())
|
body := strings.NewReader(form.Encode())
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
|
req, err := http.NewRequest("POST", endpoint, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -55,7 +53,7 @@ func FormRequest(ctx context.Context, endpoint string, request any, encoder Enco
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HttpRequest(client *http.Client, req *http.Request, response any) error {
|
func HttpRequest(client *http.Client, req *http.Request, response interface{}) error {
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -68,12 +66,7 @@ func HttpRequest(client *http.Client, req *http.Request, response any) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
var oidcErr oidc.Error
|
return fmt.Errorf("http status not ok: %s %s", resp.Status, body)
|
||||||
err = json.Unmarshal(body, &oidcErr)
|
|
||||||
if err != nil || oidcErr.ErrorType == "" {
|
|
||||||
return fmt.Errorf("http status not ok: %s %s", resp.Status, body)
|
|
||||||
}
|
|
||||||
return &oidcErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(body, response)
|
err = json.Unmarshal(body, response)
|
||||||
|
@ -83,7 +76,7 @@ func HttpRequest(client *http.Client, req *http.Request, response any) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func URLEncodeParams(resp any, encoder Encoder) (url.Values, error) {
|
func URLEncodeParams(resp interface{}, encoder Encoder) (url.Values, error) {
|
||||||
values := make(map[string][]string)
|
values := make(map[string][]string)
|
||||||
err := encoder.Encode(resp, values)
|
err := encoder.Encode(resp, values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -8,11 +8,11 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MarshalJSON(w http.ResponseWriter, i any) {
|
func MarshalJSON(w http.ResponseWriter, i interface{}) {
|
||||||
MarshalJSONWithStatus(w, i, http.StatusOK)
|
MarshalJSONWithStatus(w, i, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarshalJSONWithStatus(w http.ResponseWriter, i any, status int) {
|
func MarshalJSONWithStatus(w http.ResponseWriter, i interface{}, status int) {
|
||||||
w.Header().Set("content-type", "application/json")
|
w.Header().Set("content-type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
if i == nil || (reflect.ValueOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) {
|
if i == nil || (reflect.ValueOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) {
|
||||||
|
|
|
@ -94,7 +94,7 @@ func TestConcatenateJSON(t *testing.T) {
|
||||||
|
|
||||||
func TestMarshalJSONWithStatus(t *testing.T) {
|
func TestMarshalJSONWithStatus(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
i any
|
i interface{}
|
||||||
status int
|
status int
|
||||||
}
|
}
|
||||||
type res struct {
|
type res struct {
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ScopeOpenID defines the scope `openid`
|
// ScopeOpenID defines the scope `openid`
|
||||||
// OpenID Connect requests MUST contain the `openid` scope value
|
// OpenID Connect requests MUST contain the `openid` scope value
|
||||||
|
@ -48,7 +44,6 @@ const (
|
||||||
|
|
||||||
ResponseModeQuery ResponseMode = "query"
|
ResponseModeQuery ResponseMode = "query"
|
||||||
ResponseModeFragment ResponseMode = "fragment"
|
ResponseModeFragment ResponseMode = "fragment"
|
||||||
ResponseModeFormPost ResponseMode = "form_post"
|
|
||||||
|
|
||||||
// PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages.
|
// PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages.
|
||||||
// An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed
|
// An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed
|
||||||
|
@ -65,7 +60,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthRequest according to:
|
// AuthRequest according to:
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
//https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||||
type AuthRequest struct {
|
type AuthRequest struct {
|
||||||
Scopes SpaceDelimitedArray `json:"scope" schema:"scope"`
|
Scopes SpaceDelimitedArray `json:"scope" schema:"scope"`
|
||||||
ResponseType ResponseType `json:"response_type" schema:"response_type"`
|
ResponseType ResponseType `json:"response_type" schema:"response_type"`
|
||||||
|
@ -82,7 +77,7 @@ type AuthRequest struct {
|
||||||
UILocales Locales `json:"ui_locales" schema:"ui_locales"`
|
UILocales Locales `json:"ui_locales" schema:"ui_locales"`
|
||||||
IDTokenHint string `json:"id_token_hint" schema:"id_token_hint"`
|
IDTokenHint string `json:"id_token_hint" schema:"id_token_hint"`
|
||||||
LoginHint string `json:"login_hint" schema:"login_hint"`
|
LoginHint string `json:"login_hint" schema:"login_hint"`
|
||||||
ACRValues SpaceDelimitedArray `json:"acr_values" schema:"acr_values"`
|
ACRValues []string `json:"acr_values" schema:"acr_values"`
|
||||||
|
|
||||||
CodeChallenge string `json:"code_challenge" schema:"code_challenge"`
|
CodeChallenge string `json:"code_challenge" schema:"code_challenge"`
|
||||||
CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method" schema:"code_challenge_method"`
|
CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method" schema:"code_challenge_method"`
|
||||||
|
@ -91,15 +86,6 @@ type AuthRequest struct {
|
||||||
RequestParam string `schema:"request"`
|
RequestParam string `schema:"request"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AuthRequest) LogValue() slog.Value {
|
|
||||||
return slog.GroupValue(
|
|
||||||
slog.Any("scopes", a.Scopes),
|
|
||||||
slog.String("response_type", string(a.ResponseType)),
|
|
||||||
slog.String("client_id", a.ClientID),
|
|
||||||
slog.String("redirect_uri", a.RedirectURI),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRedirectURI returns the redirect_uri value for the ErrAuthRequest interface
|
// GetRedirectURI returns the redirect_uri value for the ErrAuthRequest interface
|
||||||
func (a *AuthRequest) GetRedirectURI() string {
|
func (a *AuthRequest) GetRedirectURI() string {
|
||||||
return a.RedirectURI
|
return a.RedirectURI
|
||||||
|
@ -114,8 +100,3 @@ func (a *AuthRequest) GetResponseType() ResponseType {
|
||||||
func (a *AuthRequest) GetState() string {
|
func (a *AuthRequest) GetState() string {
|
||||||
return a.State
|
return a.State
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetResponseMode returns the optional ResponseMode
|
|
||||||
func (a *AuthRequest) GetResponseMode() ResponseMode {
|
|
||||||
return a.ResponseMode
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
//go:build go1.20
|
|
||||||
|
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAuthRequest_LogValue(t *testing.T) {
|
|
||||||
a := &AuthRequest{
|
|
||||||
Scopes: SpaceDelimitedArray{"a", "b"},
|
|
||||||
ResponseType: "respType",
|
|
||||||
ClientID: "123",
|
|
||||||
RedirectURI: "http://example.com/callback",
|
|
||||||
}
|
|
||||||
want := slog.GroupValue(
|
|
||||||
slog.Any("scopes", SpaceDelimitedArray{"a", "b"}),
|
|
||||||
slog.String("response_type", "respType"),
|
|
||||||
slog.String("client_id", "123"),
|
|
||||||
slog.String("redirect_uri", "http://example.com/callback"),
|
|
||||||
)
|
|
||||||
got := a.LogValue()
|
|
||||||
assert.Equal(t, want, got)
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@ package oidc
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
|
"github.com/zitadel/oidc/v2/pkg/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import "encoding/json"
|
|
||||||
|
|
||||||
// DeviceAuthorizationRequest implements
|
// DeviceAuthorizationRequest implements
|
||||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1,
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1,
|
||||||
// 3.1 Device Authorization Request.
|
// 3.1 Device Authorization Request.
|
||||||
|
@ -22,26 +20,6 @@ type DeviceAuthorizationResponse struct {
|
||||||
Interval int `json:"interval,omitempty"`
|
Interval int `json:"interval,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resp *DeviceAuthorizationResponse) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias DeviceAuthorizationResponse
|
|
||||||
aux := &struct {
|
|
||||||
// workaround misspelling of verification_uri
|
|
||||||
// https://stackoverflow.com/q/76696956/5690223
|
|
||||||
// https://developers.google.com/identity/protocols/oauth2/limited-input-device?hl=fr#success-response
|
|
||||||
VerificationURL string `json:"verification_url"`
|
|
||||||
*Alias
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(resp),
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &aux); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp.VerificationURI == "" {
|
|
||||||
resp.VerificationURI = aux.VerificationURL
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeviceAccessTokenRequest implements
|
// DeviceAccessTokenRequest implements
|
||||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4,
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4,
|
||||||
// Device Access Token Request.
|
// Device Access Token Request.
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDeviceAuthorizationResponse_UnmarshalJSON(t *testing.T) {
|
|
||||||
jsonStr := `{
|
|
||||||
"device_code": "deviceCode",
|
|
||||||
"user_code": "userCode",
|
|
||||||
"verification_url": "http://example.com/verify",
|
|
||||||
"expires_in": 3600,
|
|
||||||
"interval": 5
|
|
||||||
}`
|
|
||||||
|
|
||||||
expected := &DeviceAuthorizationResponse{
|
|
||||||
DeviceCode: "deviceCode",
|
|
||||||
UserCode: "userCode",
|
|
||||||
VerificationURI: "http://example.com/verify",
|
|
||||||
ExpiresIn: 3600,
|
|
||||||
Interval: 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp DeviceAuthorizationResponse
|
|
||||||
err := resp.UnmarshalJSON([]byte(jsonStr))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, &resp)
|
|
||||||
}
|
|
|
@ -1,5 +1,9 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DiscoveryEndpoint = "/.well-known/openid-configuration"
|
DiscoveryEndpoint = "/.well-known/openid-configuration"
|
||||||
)
|
)
|
||||||
|
@ -126,10 +130,10 @@ type DiscoveryConfiguration struct {
|
||||||
ServiceDocumentation string `json:"service_documentation,omitempty"`
|
ServiceDocumentation string `json:"service_documentation,omitempty"`
|
||||||
|
|
||||||
// ClaimsLocalesSupported contains a list of BCP47 language tag values that the OP supports for values of Claims returned.
|
// ClaimsLocalesSupported contains a list of BCP47 language tag values that the OP supports for values of Claims returned.
|
||||||
ClaimsLocalesSupported Locales `json:"claims_locales_supported,omitempty"`
|
ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"`
|
||||||
|
|
||||||
// UILocalesSupported contains a list of BCP47 language tag values that the OP supports for the user interface.
|
// UILocalesSupported contains a list of BCP47 language tag values that the OP supports for the user interface.
|
||||||
UILocalesSupported Locales `json:"ui_locales_supported,omitempty"`
|
UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"`
|
||||||
|
|
||||||
// RequestParameterSupported specifies whether the OP supports use of the `request` parameter. If omitted, the default value is false.
|
// RequestParameterSupported specifies whether the OP supports use of the `request` parameter. If omitted, the default value is false.
|
||||||
RequestParameterSupported bool `json:"request_parameter_supported,omitempty"`
|
RequestParameterSupported bool `json:"request_parameter_supported,omitempty"`
|
||||||
|
@ -145,14 +149,6 @@ type DiscoveryConfiguration struct {
|
||||||
|
|
||||||
// OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service.
|
// OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service.
|
||||||
OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
|
OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
|
||||||
|
|
||||||
// BackChannelLogoutSupported specifies whether the OP supports back-channel logout (https://openid.net/specs/openid-connect-backchannel-1_0.html),
|
|
||||||
// with true indicating support. If omitted, the default value is false.
|
|
||||||
BackChannelLogoutSupported bool `json:"backchannel_logout_supported,omitempty"`
|
|
||||||
|
|
||||||
// BackChannelLogoutSessionSupported specifies whether the OP can pass a sid (session ID) Claim in the Logout Token to identify the RP session with the OP.
|
|
||||||
// If supported, the sid Claim is also included in ID Tokens issued by the OP. If omitted, the default value is false.
|
|
||||||
BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthMethod string
|
type AuthMethod string
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type errorType string
|
type errorType string
|
||||||
|
@ -28,11 +26,6 @@ const (
|
||||||
SlowDown errorType = "slow_down"
|
SlowDown errorType = "slow_down"
|
||||||
AccessDenied errorType = "access_denied"
|
AccessDenied errorType = "access_denied"
|
||||||
ExpiredToken errorType = "expired_token"
|
ExpiredToken errorType = "expired_token"
|
||||||
|
|
||||||
// InvalidTarget error is returned by Token Exchange if
|
|
||||||
// the requested target or audience is invalid.
|
|
||||||
// [RFC 8693, Section 2.2.2: Error Response](https://www.rfc-editor.org/rfc/rfc8693#section-2.2.2)
|
|
||||||
InvalidTarget errorType = "invalid_target"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -118,14 +111,6 @@ var (
|
||||||
Description: "The \"device_code\" has expired.",
|
Description: "The \"device_code\" has expired.",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token exchange error
|
|
||||||
ErrInvalidTarget = func() *Error {
|
|
||||||
return &Error{
|
|
||||||
ErrorType: InvalidTarget,
|
|
||||||
Description: "The requested audience or target is invalid.",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
|
@ -133,28 +118,7 @@ type Error struct {
|
||||||
ErrorType errorType `json:"error" schema:"error"`
|
ErrorType errorType `json:"error" schema:"error"`
|
||||||
Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
|
Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
|
||||||
State string `json:"state,omitempty" schema:"state,omitempty"`
|
State string `json:"state,omitempty" schema:"state,omitempty"`
|
||||||
SessionState string `json:"session_state,omitempty" schema:"session_state,omitempty"`
|
|
||||||
redirectDisabled bool `schema:"-"`
|
redirectDisabled bool `schema:"-"`
|
||||||
returnParent bool `schema:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Error) MarshalJSON() ([]byte, error) {
|
|
||||||
m := struct {
|
|
||||||
Error errorType `json:"error"`
|
|
||||||
ErrorDescription string `json:"error_description,omitempty"`
|
|
||||||
State string `json:"state,omitempty"`
|
|
||||||
SessionState string `json:"session_state,omitempty"`
|
|
||||||
Parent string `json:"parent,omitempty"`
|
|
||||||
}{
|
|
||||||
Error: e.ErrorType,
|
|
||||||
ErrorDescription: e.Description,
|
|
||||||
State: e.State,
|
|
||||||
SessionState: e.SessionState,
|
|
||||||
}
|
|
||||||
if e.returnParent {
|
|
||||||
m.Parent = e.Parent.Error()
|
|
||||||
}
|
|
||||||
return json.Marshal(m)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
func (e *Error) Error() string {
|
||||||
|
@ -179,8 +143,7 @@ func (e *Error) Is(target error) bool {
|
||||||
}
|
}
|
||||||
return e.ErrorType == t.ErrorType &&
|
return e.ErrorType == t.ErrorType &&
|
||||||
(e.Description == t.Description || t.Description == "") &&
|
(e.Description == t.Description || t.Description == "") &&
|
||||||
(e.State == t.State || t.State == "") &&
|
(e.State == t.State || t.State == "")
|
||||||
(e.SessionState == t.SessionState || t.SessionState == "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Error) WithParent(err error) *Error {
|
func (e *Error) WithParent(err error) *Error {
|
||||||
|
@ -188,19 +151,7 @@ func (e *Error) WithParent(err error) *Error {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithReturnParentToClient allows returning the set parent error to the HTTP client.
|
func (e *Error) WithDescription(desc string, args ...interface{}) *Error {
|
||||||
// Currently it only supports setting the parent inside JSON responses, not redirect URLs.
|
|
||||||
// As Go errors don't unmarshal well, only the marshaller is implemented for the moment.
|
|
||||||
//
|
|
||||||
// Warning: parent errors may contain sensitive data or unwanted details about the server status.
|
|
||||||
// Also, the `parent` field is not a standard error field and might confuse certain clients
|
|
||||||
// that require fully compliant responses.
|
|
||||||
func (e *Error) WithReturnParentToClient(b bool) *Error {
|
|
||||||
e.returnParent = b
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Error) WithDescription(desc string, args ...any) *Error {
|
|
||||||
e.Description = fmt.Sprintf(desc, args...)
|
e.Description = fmt.Sprintf(desc, args...)
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
@ -220,37 +171,3 @@ func DefaultToServerError(err error, description string) *Error {
|
||||||
}
|
}
|
||||||
return oauth
|
return oauth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Error) LogLevel() slog.Level {
|
|
||||||
level := slog.LevelWarn
|
|
||||||
if e.ErrorType == ServerError {
|
|
||||||
level = slog.LevelError
|
|
||||||
}
|
|
||||||
if e.ErrorType == AuthorizationPending {
|
|
||||||
level = slog.LevelInfo
|
|
||||||
}
|
|
||||||
return level
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Error) LogValue() slog.Value {
|
|
||||||
attrs := make([]slog.Attr, 0, 5)
|
|
||||||
if e.Parent != nil {
|
|
||||||
attrs = append(attrs, slog.Any("parent", e.Parent))
|
|
||||||
}
|
|
||||||
if e.Description != "" {
|
|
||||||
attrs = append(attrs, slog.String("description", e.Description))
|
|
||||||
}
|
|
||||||
if e.ErrorType != "" {
|
|
||||||
attrs = append(attrs, slog.String("type", string(e.ErrorType)))
|
|
||||||
}
|
|
||||||
if e.State != "" {
|
|
||||||
attrs = append(attrs, slog.String("state", e.State))
|
|
||||||
}
|
|
||||||
if e.SessionState != "" {
|
|
||||||
attrs = append(attrs, slog.String("session_state", e.SessionState))
|
|
||||||
}
|
|
||||||
if e.redirectDisabled {
|
|
||||||
attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled))
|
|
||||||
}
|
|
||||||
return slog.GroupValue(attrs...)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,192 +0,0 @@
|
||||||
package oidc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDefaultToServerError(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
err error
|
|
||||||
description string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want *Error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "default",
|
|
||||||
args: args{
|
|
||||||
err: io.ErrClosedPipe,
|
|
||||||
description: "oops",
|
|
||||||
},
|
|
||||||
want: &Error{
|
|
||||||
ErrorType: ServerError,
|
|
||||||
Description: "oops",
|
|
||||||
Parent: io.ErrClosedPipe,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "our Error",
|
|
||||||
args: args{
|
|
||||||
err: ErrAccessDenied(),
|
|
||||||
description: "oops",
|
|
||||||
},
|
|
||||||
want: &Error{
|
|
||||||
ErrorType: AccessDenied,
|
|
||||||
Description: "The authorization request was denied.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := DefaultToServerError(tt.args.err, tt.args.description)
|
|
||||||
assert.ErrorIs(t, got, tt.want)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestError_LogLevel(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
err *Error
|
|
||||||
want slog.Level
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "server error",
|
|
||||||
err: ErrServerError(),
|
|
||||||
want: slog.LevelError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "authorization pending",
|
|
||||||
err: ErrAuthorizationPending(),
|
|
||||||
want: slog.LevelInfo,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "some other error",
|
|
||||||
err: ErrAccessDenied(),
|
|
||||||
want: slog.LevelWarn,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := tt.err.LogLevel()
|
|
||||||
assert.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestError_LogValue(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
Parent error
|
|
||||||
ErrorType errorType
|
|
||||||
Description string
|
|
||||||
State string
|
|
||||||
redirectDisabled bool
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
want slog.Value
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "parent",
|
|
||||||
fields: fields{
|
|
||||||
Parent: io.EOF,
|
|
||||||
},
|
|
||||||
want: slog.GroupValue(slog.Any("parent", io.EOF)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "description",
|
|
||||||
fields: fields{
|
|
||||||
Description: "oops",
|
|
||||||
},
|
|
||||||
want: slog.GroupValue(slog.String("description", "oops")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "errorType",
|
|
||||||
fields: fields{
|
|
||||||
ErrorType: ExpiredToken,
|
|
||||||
},
|
|
||||||
want: slog.GroupValue(slog.String("type", string(ExpiredToken))),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "state",
|
|
||||||
fields: fields{
|
|
||||||
State: "123",
|
|
||||||
},
|
|
||||||
want: slog.GroupValue(slog.String("state", "123")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "all fields",
|
|
||||||
fields: fields{
|
|
||||||
Parent: io.EOF,
|
|
||||||
Description: "oops",
|
|
||||||
ErrorType: ExpiredToken,
|
|
||||||
State: "123",
|
|
||||||
},
|
|
||||||
want: slog.GroupValue(
|
|
||||||
slog.Any("parent", io.EOF),
|
|
||||||
slog.String("description", "oops"),
|
|
||||||
slog.String("type", string(ExpiredToken)),
|
|
||||||
slog.String("state", "123"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
e := &Error{
|
|
||||||
Parent: tt.fields.Parent,
|
|
||||||
ErrorType: tt.fields.ErrorType,
|
|
||||||
Description: tt.fields.Description,
|
|
||||||
State: tt.fields.State,
|
|
||||||
redirectDisabled: tt.fields.redirectDisabled,
|
|
||||||
}
|
|
||||||
got := e.LogValue()
|
|
||||||
assert.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestError_MarshalJSON(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
e *Error
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple error",
|
|
||||||
e: ErrAccessDenied(),
|
|
||||||
want: `{"error":"access_denied","error_description":"The authorization request was denied."}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with description",
|
|
||||||
e: ErrAccessDenied().WithDescription("oops"),
|
|
||||||
want: `{"error":"access_denied","error_description":"oops"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with parent",
|
|
||||||
e: ErrServerError().WithParent(errors.New("oops")),
|
|
||||||
want: `{"error":"server_error"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with return parent",
|
|
||||||
e: ErrServerError().WithParent(errors.New("oops")).WithReturnParentToClient(true),
|
|
||||||
want: `{"error":"server_error","parent":"oops"}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := json.Marshal(tt.e)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.JSONEq(t, tt.want, string(got))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,21 +16,18 @@ type ClientAssertionParams struct {
|
||||||
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2.
|
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2.
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims.
|
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims.
|
||||||
type IntrospectionResponse struct {
|
type IntrospectionResponse struct {
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
Scope SpaceDelimitedArray `json:"scope,omitempty"`
|
Scope SpaceDelimitedArray `json:"scope,omitempty"`
|
||||||
ClientID string `json:"client_id,omitempty"`
|
ClientID string `json:"client_id,omitempty"`
|
||||||
TokenType string `json:"token_type,omitempty"`
|
TokenType string `json:"token_type,omitempty"`
|
||||||
Expiration Time `json:"exp,omitempty"`
|
Expiration Time `json:"exp,omitempty"`
|
||||||
IssuedAt Time `json:"iat,omitempty"`
|
IssuedAt Time `json:"iat,omitempty"`
|
||||||
AuthTime Time `json:"auth_time,omitempty"`
|
NotBefore Time `json:"nbf,omitempty"`
|
||||||
NotBefore Time `json:"nbf,omitempty"`
|
Subject string `json:"sub,omitempty"`
|
||||||
Subject string `json:"sub,omitempty"`
|
Audience Audience `json:"aud,omitempty"`
|
||||||
Audience Audience `json:"aud,omitempty"`
|
Issuer string `json:"iss,omitempty"`
|
||||||
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
JWTID string `json:"jti,omitempty"`
|
||||||
Issuer string `json:"iss,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
JWTID string `json:"jti,omitempty"`
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
Actor *ActorClaims `json:"act,omitempty"`
|
|
||||||
UserInfoProfile
|
UserInfoProfile
|
||||||
UserInfoEmail
|
UserInfoEmail
|
||||||
UserInfoPhone
|
UserInfoPhone
|
||||||
|
|
|
@ -6,9 +6,8 @@ import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -47,8 +46,8 @@ func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) {
|
||||||
//
|
//
|
||||||
// will return false none or multiple match
|
// will return false none or multiple match
|
||||||
//
|
//
|
||||||
// deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool
|
//deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool
|
||||||
// moved implementation already to FindMatchingKey
|
//moved implementation already to FindMatchingKey
|
||||||
func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) {
|
func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) {
|
||||||
key, err := FindMatchingKey(keyID, use, expectedAlg, keys...)
|
key, err := FindMatchingKey(keyID, use, expectedAlg, keys...)
|
||||||
return key, err == nil
|
return key, err == nil
|
||||||
|
@ -92,18 +91,18 @@ func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (k
|
||||||
return key, ErrKeyNone
|
return key, ErrKeyNone
|
||||||
}
|
}
|
||||||
|
|
||||||
func algToKeyType(key any, alg string) bool {
|
func algToKeyType(key interface{}, alg string) bool {
|
||||||
if strings.HasPrefix(alg, "RS") || strings.HasPrefix(alg, "PS") {
|
switch alg[0] {
|
||||||
|
case 'R', 'P':
|
||||||
_, ok := key.(*rsa.PublicKey)
|
_, ok := key.(*rsa.PublicKey)
|
||||||
return ok
|
return ok
|
||||||
}
|
case 'E':
|
||||||
if strings.HasPrefix(alg, "ES") {
|
|
||||||
_, ok := key.(*ecdsa.PublicKey)
|
_, ok := key.(*ecdsa.PublicKey)
|
||||||
return ok
|
return ok
|
||||||
}
|
case 'O':
|
||||||
if alg == string(jose.EdDSA) {
|
_, ok := key.(*ed25519.PublicKey)
|
||||||
_, ok := key.(ed25519.PublicKey)
|
|
||||||
return ok
|
return ok
|
||||||
|
default:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFindKey(t *testing.T) {
|
func TestFindKey(t *testing.T) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ const dataDir = "regression_data"
|
||||||
|
|
||||||
// jsonFilename builds a filename for the regression testdata.
|
// jsonFilename builds a filename for the regression testdata.
|
||||||
// dataDir/<type_name>.json
|
// dataDir/<type_name>.json
|
||||||
func jsonFilename(obj any) string {
|
func jsonFilename(obj interface{}) string {
|
||||||
name := fmt.Sprintf("%T.json", obj)
|
name := fmt.Sprintf("%T.json", obj)
|
||||||
return path.Join(
|
return path.Join(
|
||||||
dataDir,
|
dataDir,
|
||||||
|
@ -25,13 +25,13 @@ func jsonFilename(obj any) string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeJSON(t *testing.T, w io.Writer, obj any) {
|
func encodeJSON(t *testing.T, w io.Writer, obj interface{}) {
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
enc.SetIndent("", "\t")
|
enc.SetIndent("", "\t")
|
||||||
require.NoError(t, enc.Encode(obj))
|
require.NoError(t, enc.Encode(obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
var regressionData = []any{
|
var regressionData = []interface{}{
|
||||||
accessTokenData,
|
accessTokenData,
|
||||||
idTokenData,
|
idTokenData,
|
||||||
introspectionResponseData,
|
introspectionResponseData,
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
// EndSessionRequest for the RP-Initiated Logout according to:
|
// EndSessionRequest for the RP-Initiated Logout according to:
|
||||||
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
|
//https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
|
||||||
type EndSessionRequest struct {
|
type EndSessionRequest struct {
|
||||||
IdTokenHint string `schema:"id_token_hint"`
|
IdTokenHint string `schema:"id_token_hint"`
|
||||||
LogoutHint string `schema:"logout_hint"`
|
ClientID string `schema:"client_id"`
|
||||||
ClientID string `schema:"client_id"`
|
PostLogoutRedirectURI string `schema:"post_logout_redirect_uri"`
|
||||||
PostLogoutRedirectURI string `schema:"post_logout_redirect_uri"`
|
State string `schema:"state"`
|
||||||
State string `schema:"state"`
|
|
||||||
UILocales Locales `schema:"ui_locales"`
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,11 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/crypto"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -35,20 +34,19 @@ type Tokens[C IDClaims] struct {
|
||||||
// TokenClaims implements the Claims interface,
|
// TokenClaims implements the Claims interface,
|
||||||
// and can be used to extend larger claim types by embedding.
|
// and can be used to extend larger claim types by embedding.
|
||||||
type TokenClaims struct {
|
type TokenClaims struct {
|
||||||
Issuer string `json:"iss,omitempty"`
|
Issuer string `json:"iss,omitempty"`
|
||||||
Subject string `json:"sub,omitempty"`
|
Subject string `json:"sub,omitempty"`
|
||||||
Audience Audience `json:"aud,omitempty"`
|
Audience Audience `json:"aud,omitempty"`
|
||||||
Expiration Time `json:"exp,omitempty"`
|
Expiration Time `json:"exp,omitempty"`
|
||||||
IssuedAt Time `json:"iat,omitempty"`
|
IssuedAt Time `json:"iat,omitempty"`
|
||||||
AuthTime Time `json:"auth_time,omitempty"`
|
AuthTime Time `json:"auth_time,omitempty"`
|
||||||
NotBefore Time `json:"nbf,omitempty"`
|
NotBefore Time `json:"nbf,omitempty"`
|
||||||
Nonce string `json:"nonce,omitempty"`
|
Nonce string `json:"nonce,omitempty"`
|
||||||
AuthenticationContextClassReference string `json:"acr,omitempty"`
|
AuthenticationContextClassReference string `json:"acr,omitempty"`
|
||||||
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
||||||
AuthorizedParty string `json:"azp,omitempty"`
|
AuthorizedParty string `json:"azp,omitempty"`
|
||||||
ClientID string `json:"client_id,omitempty"`
|
ClientID string `json:"client_id,omitempty"`
|
||||||
JWTID string `json:"jti,omitempty"`
|
JWTID string `json:"jti,omitempty"`
|
||||||
Actor *ActorClaims `json:"act,omitempty"`
|
|
||||||
|
|
||||||
// Additional information set by this framework
|
// Additional information set by this framework
|
||||||
SignatureAlg jose.SignatureAlgorithm `json:"-"`
|
SignatureAlg jose.SignatureAlgorithm `json:"-"`
|
||||||
|
@ -117,7 +115,6 @@ func NewAccessTokenClaims(issuer, subject string, audience []string, expiration
|
||||||
Expiration: FromTime(expiration),
|
Expiration: FromTime(expiration),
|
||||||
IssuedAt: FromTime(now),
|
IssuedAt: FromTime(now),
|
||||||
NotBefore: FromTime(now),
|
NotBefore: FromTime(now),
|
||||||
ClientID: clientID,
|
|
||||||
JWTID: jwtid,
|
JWTID: jwtid,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -207,36 +204,13 @@ func (i *IDTokenClaims) UnmarshalJSON(data []byte) error {
|
||||||
return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims)
|
return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActorClaims provides the `act` claims used for impersonation or delegation Token Exchange.
|
|
||||||
//
|
|
||||||
// An actor can be nested in case an obtained token is used as actor token to obtain impersonation or delegation.
|
|
||||||
// This allows creating a chain of actors.
|
|
||||||
// See [RFC 8693, section 4.1](https://www.rfc-editor.org/rfc/rfc8693#name-act-actor-claim).
|
|
||||||
type ActorClaims struct {
|
|
||||||
Actor *ActorClaims `json:"act,omitempty"`
|
|
||||||
Issuer string `json:"iss,omitempty"`
|
|
||||||
Subject string `json:"sub,omitempty"`
|
|
||||||
Claims map[string]any `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type acAlias ActorClaims
|
|
||||||
|
|
||||||
func (c *ActorClaims) MarshalJSON() ([]byte, error) {
|
|
||||||
return mergeAndMarshalClaims((*acAlias)(c), c.Claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ActorClaims) UnmarshalJSON(data []byte) error {
|
|
||||||
return unmarshalJSONMulti(data, (*acAlias)(c), &c.Claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccessTokenResponse struct {
|
type AccessTokenResponse struct {
|
||||||
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
|
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
|
||||||
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
|
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
|
||||||
RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
|
RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
|
||||||
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
|
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
|
||||||
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
|
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
|
||||||
State string `json:"state,omitempty" schema:"state,omitempty"`
|
State string `json:"state,omitempty" schema:"state,omitempty"`
|
||||||
Scope SpaceDelimitedArray `json:"scope,omitempty" schema:"scope,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWTProfileAssertionClaims struct {
|
type JWTProfileAssertionClaims struct {
|
||||||
|
@ -248,7 +222,7 @@ type JWTProfileAssertionClaims struct {
|
||||||
Expiration Time `json:"exp"`
|
Expiration Time `json:"exp"`
|
||||||
IssuedAt Time `json:"iat"`
|
IssuedAt Time `json:"iat"`
|
||||||
|
|
||||||
Claims map[string]any `json:"-"`
|
Claims map[string]interface{} `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jpaAlias JWTProfileAssertionClaims
|
type jpaAlias JWTProfileAssertionClaims
|
||||||
|
@ -288,7 +262,7 @@ func JWTProfileDelegatedSubject(sub string) func(*JWTProfileAssertionClaims) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func JWTProfileCustomClaim(key string, value any) func(*JWTProfileAssertionClaims) {
|
func JWTProfileCustomClaim(key string, value interface{}) func(*JWTProfileAssertionClaims) {
|
||||||
return func(j *JWTProfileAssertionClaims) {
|
return func(j *JWTProfileAssertionClaims) {
|
||||||
j.Claims[key] = value
|
j.Claims[key] = value
|
||||||
}
|
}
|
||||||
|
@ -318,7 +292,7 @@ func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte,
|
||||||
IssuedAt: FromTime(time.Now().UTC()),
|
IssuedAt: FromTime(time.Now().UTC()),
|
||||||
Expiration: FromTime(time.Now().Add(1 * time.Hour).UTC()),
|
Expiration: FromTime(time.Now().Add(1 * time.Hour).UTC()),
|
||||||
Audience: audience,
|
Audience: audience,
|
||||||
Claims: make(map[string]any),
|
Claims: make(map[string]interface{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
@ -347,12 +321,12 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) {
|
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) {
|
||||||
privateKey, algorithm, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
|
privateKey, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
key := jose.SigningKey{
|
key := jose.SigningKey{
|
||||||
Algorithm: algorithm,
|
Algorithm: jose.RS256,
|
||||||
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
|
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
|
||||||
}
|
}
|
||||||
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
||||||
|
@ -378,45 +352,4 @@ type TokenExchangeResponse struct {
|
||||||
ExpiresIn uint64 `json:"expires_in,omitempty"`
|
ExpiresIn uint64 `json:"expires_in,omitempty"`
|
||||||
Scopes SpaceDelimitedArray `json:"scope,omitempty"`
|
Scopes SpaceDelimitedArray `json:"scope,omitempty"`
|
||||||
RefreshToken string `json:"refresh_token,omitempty"`
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
|
|
||||||
// IDToken field allows returning an additional ID token
|
|
||||||
// if the requested_token_type was Access Token and scope contained openid.
|
|
||||||
IDToken string `json:"id_token,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogoutTokenClaims struct {
|
|
||||||
Issuer string `json:"iss,omitempty"`
|
|
||||||
Subject string `json:"sub,omitempty"`
|
|
||||||
Audience Audience `json:"aud,omitempty"`
|
|
||||||
IssuedAt Time `json:"iat,omitempty"`
|
|
||||||
Expiration Time `json:"exp,omitempty"`
|
|
||||||
JWTID string `json:"jti,omitempty"`
|
|
||||||
Events map[string]any `json:"events,omitempty"`
|
|
||||||
SessionID string `json:"sid,omitempty"`
|
|
||||||
Claims map[string]any `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ltcAlias LogoutTokenClaims
|
|
||||||
|
|
||||||
func (i *LogoutTokenClaims) MarshalJSON() ([]byte, error) {
|
|
||||||
return mergeAndMarshalClaims((*ltcAlias)(i), i.Claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *LogoutTokenClaims) UnmarshalJSON(data []byte) error {
|
|
||||||
return unmarshalJSONMulti(data, (*ltcAlias)(i), &i.Claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLogoutTokenClaims(issuer, subject string, audience Audience, expiration time.Time, jwtID, sessionID string, skew time.Duration) *LogoutTokenClaims {
|
|
||||||
return &LogoutTokenClaims{
|
|
||||||
Issuer: issuer,
|
|
||||||
Subject: subject,
|
|
||||||
Audience: audience,
|
|
||||||
IssuedAt: FromTime(time.Now().Add(-skew)),
|
|
||||||
Expiration: FromTime(expiration),
|
|
||||||
JWTID: jwtID,
|
|
||||||
Events: map[string]any{
|
|
||||||
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
|
||||||
},
|
|
||||||
SessionID: sessionID,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,9 @@ package oidc
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -58,7 +57,13 @@ var AllTokenTypes = []TokenType{
|
||||||
type TokenType string
|
type TokenType string
|
||||||
|
|
||||||
func (t TokenType) IsSupported() bool {
|
func (t TokenType) IsSupported() bool {
|
||||||
return slices.Contains(AllTokenTypes, t)
|
for _, tt := range AllTokenTypes {
|
||||||
|
if t == tt {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenRequest interface {
|
type TokenRequest interface {
|
||||||
|
@ -72,10 +77,10 @@ type AccessTokenRequest struct {
|
||||||
Code string `schema:"code"`
|
Code string `schema:"code"`
|
||||||
RedirectURI string `schema:"redirect_uri"`
|
RedirectURI string `schema:"redirect_uri"`
|
||||||
ClientID string `schema:"client_id"`
|
ClientID string `schema:"client_id"`
|
||||||
ClientSecret string `schema:"client_secret,omitempty"`
|
ClientSecret string `schema:"client_secret"`
|
||||||
CodeVerifier string `schema:"code_verifier,omitempty"`
|
CodeVerifier string `schema:"code_verifier"`
|
||||||
ClientAssertion string `schema:"client_assertion,omitempty"`
|
ClientAssertion string `schema:"client_assertion"`
|
||||||
ClientAssertionType string `schema:"client_assertion_type,omitempty"`
|
ClientAssertionType string `schema:"client_assertion_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AccessTokenRequest) GrantType() GrantType {
|
func (a *AccessTokenRequest) GrantType() GrantType {
|
||||||
|
@ -125,7 +130,7 @@ type JWTTokenRequest struct {
|
||||||
IssuedAt Time `json:"iat"`
|
IssuedAt Time `json:"iat"`
|
||||||
ExpiresAt Time `json:"exp"`
|
ExpiresAt Time `json:"exp"`
|
||||||
|
|
||||||
private map[string]any
|
private map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *JWTTokenRequest) MarshalJSON() ([]byte, error) {
|
func (j *JWTTokenRequest) MarshalJSON() ([]byte, error) {
|
||||||
|
@ -166,7 +171,7 @@ func (j *JWTTokenRequest) UnmarshalJSON(data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *JWTTokenRequest) GetCustomClaim(key string) any {
|
func (j *JWTTokenRequest) GetCustomClaim(key string) interface{} {
|
||||||
return j.private[key]
|
return j.private[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +241,7 @@ type TokenExchangeRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientCredentialsRequest struct {
|
type ClientCredentialsRequest struct {
|
||||||
GrantType GrantType `schema:"grant_type,omitempty"`
|
GrantType GrantType `schema:"grant_type"`
|
||||||
Scope SpaceDelimitedArray `schema:"scope"`
|
Scope SpaceDelimitedArray `schema:"scope"`
|
||||||
ClientID string `schema:"client_id"`
|
ClientID string `schema:"client_id"`
|
||||||
ClientSecret string `schema:"client_secret"`
|
ClientSecret string `schema:"client_secret"`
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -29,7 +29,7 @@ var (
|
||||||
accessTokenData = &AccessTokenClaims{
|
accessTokenData = &AccessTokenClaims{
|
||||||
TokenClaims: tokenClaimsData,
|
TokenClaims: tokenClaimsData,
|
||||||
Scopes: []string{"email", "phone"},
|
Scopes: []string{"email", "phone"},
|
||||||
Claims: map[string]any{
|
Claims: map[string]interface{}{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ var (
|
||||||
UserInfoEmail: userInfoData.UserInfoEmail,
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
UserInfoPhone: userInfoData.UserInfoPhone,
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
Address: userInfoData.Address,
|
Address: userInfoData.Address,
|
||||||
Claims: map[string]any{
|
Claims: map[string]interface{}{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ var (
|
||||||
UserInfoEmail: userInfoData.UserInfoEmail,
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
UserInfoPhone: userInfoData.UserInfoPhone,
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
Address: userInfoData.Address,
|
Address: userInfoData.Address,
|
||||||
Claims: map[string]any{
|
Claims: map[string]interface{}{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ var (
|
||||||
PostalCode: "666-666",
|
PostalCode: "666-666",
|
||||||
Country: "Moon",
|
Country: "Moon",
|
||||||
},
|
},
|
||||||
Claims: map[string]any{
|
Claims: map[string]interface{}{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ var (
|
||||||
Audience: Audience{"foo", "bar"},
|
Audience: Audience{"foo", "bar"},
|
||||||
Expiration: 12345,
|
Expiration: 12345,
|
||||||
IssuedAt: 12000,
|
IssuedAt: 12000,
|
||||||
Claims: map[string]any{
|
Claims: map[string]interface{}{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,6 @@ func TestNewAccessTokenClaims(t *testing.T) {
|
||||||
Subject: "hello@me.com",
|
Subject: "hello@me.com",
|
||||||
Audience: Audience{"foo"},
|
Audience: Audience{"foo"},
|
||||||
Expiration: 12345,
|
Expiration: 12345,
|
||||||
ClientID: "foo",
|
|
||||||
JWTID: "900",
|
JWTID: "900",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -182,7 +181,7 @@ func TestIDTokenClaims_SetUserInfo(t *testing.T) {
|
||||||
UserInfoEmail: userInfoData.UserInfoEmail,
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
UserInfoPhone: userInfoData.UserInfoPhone,
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
Address: userInfoData.Address,
|
Address: userInfoData.Address,
|
||||||
Claims: map[string]any{
|
Claims: map[string]interface{}{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -242,39 +241,3 @@ func TestIDTokenClaims_GetUserInfo(t *testing.T) {
|
||||||
got := idTokenData.GetUserInfo()
|
got := idTokenData.GetUserInfo()
|
||||||
assert.Equal(t, want, got)
|
assert.Equal(t, want, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewLogoutTokenClaims(t *testing.T) {
|
|
||||||
want := &LogoutTokenClaims{
|
|
||||||
Issuer: "zitadel",
|
|
||||||
Subject: "hello@me.com",
|
|
||||||
Audience: Audience{"foo", "just@me.com"},
|
|
||||||
Expiration: 12345,
|
|
||||||
JWTID: "jwtID",
|
|
||||||
Events: map[string]any{
|
|
||||||
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
|
||||||
},
|
|
||||||
SessionID: "sessionID",
|
|
||||||
Claims: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
got := NewLogoutTokenClaims(
|
|
||||||
want.Issuer,
|
|
||||||
want.Subject,
|
|
||||||
want.Audience,
|
|
||||||
want.Expiration.AsTime(),
|
|
||||||
want.JWTID,
|
|
||||||
want.SessionID,
|
|
||||||
1*time.Second,
|
|
||||||
)
|
|
||||||
|
|
||||||
// test if the dynamic timestamp is around now,
|
|
||||||
// allowing for a delta of 1, just in case we flip on
|
|
||||||
// either side of a second boundry.
|
|
||||||
nowMinusSkew := NowTime() - 1
|
|
||||||
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
|
|
||||||
|
|
||||||
// Make equal not fail on dynamic timestamp
|
|
||||||
got.IssuedAt = 0
|
|
||||||
|
|
||||||
assert.Equal(t, want, got)
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,28 +3,26 @@ package oidc
|
||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"github.com/gorilla/schema"
|
||||||
"github.com/muhlemmer/gu"
|
|
||||||
"github.com/zitadel/schema"
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Audience []string
|
type Audience []string
|
||||||
|
|
||||||
func (a *Audience) UnmarshalJSON(text []byte) error {
|
func (a *Audience) UnmarshalJSON(text []byte) error {
|
||||||
var i any
|
var i interface{}
|
||||||
err := json.Unmarshal(text, &i)
|
err := json.Unmarshal(text, &i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
switch aud := i.(type) {
|
switch aud := i.(type) {
|
||||||
case []any:
|
case []interface{}:
|
||||||
*a = make([]string, len(aud))
|
*a = make([]string, len(aud))
|
||||||
for i, audience := range aud {
|
for i, audience := range aud {
|
||||||
(*a)[i] = audience.(string)
|
(*a)[i] = audience.(string)
|
||||||
|
@ -35,17 +33,6 @@ func (a *Audience) UnmarshalJSON(text []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Audience) MarshalJSON() ([]byte, error) {
|
|
||||||
len := len(*a)
|
|
||||||
if len > 1 {
|
|
||||||
return json.Marshal(*a)
|
|
||||||
} else if len == 1 {
|
|
||||||
return json.Marshal((*a)[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("aud is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Display string
|
type Display string
|
||||||
|
|
||||||
func (d *Display) UnmarshalText(text []byte) error {
|
func (d *Display) UnmarshalText(text []byte) error {
|
||||||
|
@ -88,90 +75,20 @@ func (l *Locale) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(tag)
|
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 {
|
func (l *Locale) UnmarshalJSON(data []byte) error {
|
||||||
if len(data) == 0 || string(data) == "\"\"" {
|
return json.Unmarshal(data, &l.tag)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := json.Unmarshal(data, &l.tag)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// catch "well-formed but unknown" errors
|
|
||||||
var target language.ValueError
|
|
||||||
if errors.As(err, &target) {
|
|
||||||
l.tag = language.Tag{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Locales []language.Tag
|
type Locales []language.Tag
|
||||||
|
|
||||||
// ParseLocales parses a slice of strings into Locales.
|
func (l *Locales) UnmarshalText(text []byte) error {
|
||||||
// If an entry causes a parse error or is undefined,
|
locales := strings.Split(string(text), " ")
|
||||||
// it is ignored and not set to Locales.
|
|
||||||
func ParseLocales(locales []string) Locales {
|
|
||||||
out := make(Locales, 0, len(locales))
|
|
||||||
for _, locale := range locales {
|
for _, locale := range locales {
|
||||||
tag, err := language.Parse(locale)
|
tag, err := language.Parse(locale)
|
||||||
if err == nil && !tag.IsRoot() {
|
if err == nil && !tag.IsRoot() {
|
||||||
out = append(out, tag)
|
*l = append(*l, tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Locales) String() string {
|
|
||||||
tags := make([]string, len(l))
|
|
||||||
for i, tag := range l {
|
|
||||||
tags[i] = tag.String()
|
|
||||||
}
|
|
||||||
return strings.Join(tags, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
|
|
||||||
// It decodes an unquoted space seperated string into Locales.
|
|
||||||
// Undefined language tags in the input are ignored and ommited from
|
|
||||||
// the resulting Locales.
|
|
||||||
func (l *Locales) UnmarshalText(text []byte) error {
|
|
||||||
*l = ParseLocales(
|
|
||||||
strings.Split(string(text), " "),
|
|
||||||
)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON implements the [json.Unmarshaler] interface.
|
|
||||||
// It decodes a json array or a space seperated string into Locales.
|
|
||||||
// Undefined language tags in the input are ignored and ommited from
|
|
||||||
// the resulting Locales.
|
|
||||||
func (l *Locales) UnmarshalJSON(data []byte) error {
|
|
||||||
var dst any
|
|
||||||
if err := json.Unmarshal(data, &dst); err != nil {
|
|
||||||
return fmt.Errorf("oidc locales: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We catch the posibility of a space seperated string here,
|
|
||||||
// because UnmarshalText might have been implicetely called
|
|
||||||
// by the json library before we added UnmarshalJSON.
|
|
||||||
switch v := dst.(type) {
|
|
||||||
case nil:
|
|
||||||
*l = nil
|
|
||||||
case string:
|
|
||||||
*l = ParseLocales(strings.Split(v, " "))
|
|
||||||
case []any:
|
|
||||||
locales, err := gu.AssertInterfaces[string](v)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("oidc locales: %w", err)
|
|
||||||
}
|
|
||||||
*l = ParseLocales(locales)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("oidc locales: unsupported type: %T", v)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +106,7 @@ type ResponseType string
|
||||||
|
|
||||||
type ResponseMode string
|
type ResponseMode string
|
||||||
|
|
||||||
func (s SpaceDelimitedArray) String() string {
|
func (s SpaceDelimitedArray) Encode() string {
|
||||||
return strings.Join(s, " ")
|
return strings.Join(s, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,11 +116,11 @@ func (s *SpaceDelimitedArray) UnmarshalText(text []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SpaceDelimitedArray) MarshalText() ([]byte, error) {
|
func (s SpaceDelimitedArray) MarshalText() ([]byte, error) {
|
||||||
return []byte(s.String()), nil
|
return []byte(s.Encode()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SpaceDelimitedArray) MarshalJSON() ([]byte, error) {
|
func (s SpaceDelimitedArray) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal((s).String())
|
return json.Marshal((s).Encode())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error {
|
func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error {
|
||||||
|
@ -215,7 +132,7 @@ func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SpaceDelimitedArray) Scan(src any) error {
|
func (s *SpaceDelimitedArray) Scan(src interface{}) error {
|
||||||
if src == nil {
|
if src == nil {
|
||||||
*s = nil
|
*s = nil
|
||||||
return nil
|
return nil
|
||||||
|
@ -248,10 +165,7 @@ func (s SpaceDelimitedArray) Value() (driver.Value, error) {
|
||||||
func NewEncoder() *schema.Encoder {
|
func NewEncoder() *schema.Encoder {
|
||||||
e := schema.NewEncoder()
|
e := schema.NewEncoder()
|
||||||
e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string {
|
e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string {
|
||||||
return value.Interface().(SpaceDelimitedArray).String()
|
return value.Interface().(SpaceDelimitedArray).Encode()
|
||||||
})
|
|
||||||
e.RegisterEncoder(Locales{}, func(value reflect.Value) string {
|
|
||||||
return value.Interface().(Locales).String()
|
|
||||||
})
|
})
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/schema"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/zitadel/schema"
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -208,78 +208,20 @@ func TestLocale_MarshalJSON(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocale_UnmarshalJSON(t *testing.T) {
|
func TestLocale_UnmarshalJSON(t *testing.T) {
|
||||||
type dst struct {
|
type a struct {
|
||||||
Locale *Locale `json:"locale,omitempty"`
|
Locale *Locale `json:"locale,omitempty"`
|
||||||
}
|
}
|
||||||
tests := []struct {
|
want := a{
|
||||||
name string
|
Locale: NewLocale(language.Afrikaans),
|
||||||
input string
|
|
||||||
want dst
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "value not present",
|
|
||||||
input: `{}`,
|
|
||||||
wantErr: false,
|
|
||||||
want: dst{
|
|
||||||
Locale: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "null",
|
|
||||||
input: `{"locale": null}`,
|
|
||||||
wantErr: false,
|
|
||||||
want: dst{
|
|
||||||
Locale: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty, ignored",
|
|
||||||
input: `{"locale": ""}`,
|
|
||||||
wantErr: false,
|
|
||||||
want: dst{
|
|
||||||
Locale: &Locale{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "afrikaans, ok",
|
|
||||||
input: `{"locale": "af"}`,
|
|
||||||
want: dst{
|
|
||||||
Locale: NewLocale(language.Afrikaans),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "gb, ignored",
|
|
||||||
input: `{"locale": "gb"}`,
|
|
||||||
want: dst{
|
|
||||||
Locale: &Locale{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad form, error",
|
|
||||||
input: `{"locale": "g!!!!!"}`,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
var got dst
|
|
||||||
err := json.Unmarshal([]byte(tt.input), &got)
|
|
||||||
if tt.wantErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseLocales(t *testing.T) {
|
const input = `{"locale": "af"}`
|
||||||
in := []string{language.Afrikaans.String(), language.Danish.String(), "foobar", language.Und.String()}
|
var got a
|
||||||
want := Locales{language.Afrikaans, language.Danish}
|
|
||||||
got := ParseLocales(in)
|
require.NoError(t,
|
||||||
assert.ElementsMatch(t, want, got)
|
json.Unmarshal([]byte(input), &got),
|
||||||
|
)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocales_UnmarshalText(t *testing.T) {
|
func TestLocales_UnmarshalText(t *testing.T) {
|
||||||
|
@ -339,80 +281,6 @@ func TestLocales_UnmarshalText(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocales_UnmarshalJSON(t *testing.T) {
|
|
||||||
in := []string{language.Afrikaans.String(), language.Danish.String(), "foobar", language.Und.String()}
|
|
||||||
spaceSepStr := strconv.Quote(strings.Join(in, " "))
|
|
||||||
jsonArray, err := json.Marshal(in)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
out := Locales{language.Afrikaans, language.Danish}
|
|
||||||
|
|
||||||
type args struct {
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want Locales
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "invalid JSON",
|
|
||||||
args: args{
|
|
||||||
data: []byte("~~~"),
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "null",
|
|
||||||
args: args{
|
|
||||||
data: []byte("null"),
|
|
||||||
},
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "space seperated string",
|
|
||||||
args: args{
|
|
||||||
data: []byte(spaceSepStr),
|
|
||||||
},
|
|
||||||
want: out,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "json string array",
|
|
||||||
args: args{
|
|
||||||
data: jsonArray,
|
|
||||||
},
|
|
||||||
want: out,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "json invalid array",
|
|
||||||
args: args{
|
|
||||||
data: []byte(`[1,2,3]`),
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid type (float64)",
|
|
||||||
args: args{
|
|
||||||
data: []byte("22"),
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
var got Locales
|
|
||||||
err := got.UnmarshalJSON([]byte(tt.args.data))
|
|
||||||
if tt.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestScopes_UnmarshalText(t *testing.T) {
|
func TestScopes_UnmarshalText(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
text []byte
|
text []byte
|
||||||
|
|
|
@ -29,11 +29,6 @@ func (u *UserInfo) GetAddress() *UserInfoAddress {
|
||||||
return u.Address
|
return u.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSubject implements [rp.SubjectGetter]
|
|
||||||
func (u *UserInfo) GetSubject() string {
|
|
||||||
return u.Subject
|
|
||||||
}
|
|
||||||
|
|
||||||
type uiAlias UserInfo
|
type uiAlias UserInfo
|
||||||
|
|
||||||
func (u *UserInfo) MarshalJSON() ([]byte, error) {
|
func (u *UserInfo) MarshalJSON() ([]byte, error) {
|
||||||
|
|
|
@ -7,11 +7,12 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
str "github.com/zitadel/oidc/v2/pkg/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Claims interface {
|
type Claims interface {
|
||||||
|
@ -40,7 +41,6 @@ type IDClaims interface {
|
||||||
var (
|
var (
|
||||||
ErrParse = errors.New("parsing of request failed")
|
ErrParse = errors.New("parsing of request failed")
|
||||||
ErrIssuerInvalid = errors.New("issuer does not match")
|
ErrIssuerInvalid = errors.New("issuer does not match")
|
||||||
ErrDiscoveryFailed = errors.New("OpenID Provider Configuration Discovery has failed")
|
|
||||||
ErrSubjectMissing = errors.New("subject missing")
|
ErrSubjectMissing = errors.New("subject missing")
|
||||||
ErrAudience = errors.New("audience is not valid")
|
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")
|
ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty")
|
||||||
|
@ -57,23 +57,14 @@ var (
|
||||||
ErrNonceInvalid = errors.New("nonce does not match")
|
ErrNonceInvalid = errors.New("nonce does not match")
|
||||||
ErrAcrInvalid = errors.New("acr is invalid")
|
ErrAcrInvalid = errors.New("acr is invalid")
|
||||||
ErrAuthTimeNotPresent = errors.New("claim `auth_time` of token is missing")
|
ErrAuthTimeNotPresent = errors.New("claim `auth_time` of token is missing")
|
||||||
ErrAuthTimeToOld = errors.New("auth time of token is too old")
|
ErrAuthTimeToOld = errors.New("auth time of token is to old")
|
||||||
ErrAtHash = errors.New("at_hash does not correspond to access token")
|
ErrAtHash = errors.New("at_hash does not correspond to access token")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verifier caries configuration for the various token verification
|
type Verifier interface {
|
||||||
// functions. Use package specific constructor functions to know
|
Issuer() string
|
||||||
// which values need to be set.
|
MaxAgeIAT() time.Duration
|
||||||
type Verifier struct {
|
Offset() time.Duration
|
||||||
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
|
// ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim
|
||||||
|
@ -83,7 +74,7 @@ type ACRVerifier func(string) error
|
||||||
// if none of the provided values matches the acr claim
|
// if none of the provided values matches the acr claim
|
||||||
func DefaultACRVerifier(possibleValues []string) ACRVerifier {
|
func DefaultACRVerifier(possibleValues []string) ACRVerifier {
|
||||||
return func(acr string) error {
|
return func(acr string) error {
|
||||||
if !slices.Contains(possibleValues, acr) {
|
if !str.Contains(possibleValues, acr) {
|
||||||
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
|
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -94,7 +85,7 @@ func DecryptToken(tokenString string) (string, error) {
|
||||||
return tokenString, nil // TODO: impl
|
return tokenString, nil // TODO: impl
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseToken(tokenString string, claims any) ([]byte, error) {
|
func ParseToken(tokenString string, claims interface{}) ([]byte, error) {
|
||||||
parts := strings.Split(tokenString, ".")
|
parts := strings.Split(tokenString, ".")
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
return nil, fmt.Errorf("%w: token contains an invalid number of segments", ErrParse)
|
return nil, fmt.Errorf("%w: token contains an invalid number of segments", ErrParse)
|
||||||
|
@ -122,7 +113,7 @@ func CheckIssuer(claims Claims, issuer string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckAudience(claims Claims, clientID string) error {
|
func CheckAudience(claims Claims, clientID string) error {
|
||||||
if !slices.Contains(claims.GetAudience(), clientID) {
|
if !str.Contains(claims.GetAudience(), clientID) {
|
||||||
return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID)
|
return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,11 +121,6 @@ func CheckAudience(claims Claims, clientID string) error {
|
||||||
return nil
|
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 {
|
func CheckAuthorizedParty(claims Claims, clientID string) error {
|
||||||
if len(claims.GetAudience()) > 1 {
|
if len(claims.GetAudience()) > 1 {
|
||||||
if claims.GetAuthorizedParty() == "" {
|
if claims.GetAuthorizedParty() == "" {
|
||||||
|
@ -148,13 +134,8 @@ func CheckAuthorizedParty(claims Claims, clientID string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckSignature(ctx context.Context, token string, payload []byte, claims ClaimsSignature, supportedSigAlgs []string, set KeySet) error {
|
func CheckSignature(ctx context.Context, token string, payload []byte, claims ClaimsSignature, supportedSigAlgs []string, set KeySet) error {
|
||||||
jws, err := jose.ParseSigned(token, toJoseSignatureAlgorithms(supportedSigAlgs))
|
jws, err := jose.ParseSigned(token)
|
||||||
if err != nil {
|
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
|
return ErrParse
|
||||||
}
|
}
|
||||||
if len(jws.Signatures) == 0 {
|
if len(jws.Signatures) == 0 {
|
||||||
|
@ -164,6 +145,12 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl
|
||||||
return ErrSignatureMultiple
|
return ErrSignatureMultiple
|
||||||
}
|
}
|
||||||
sig := jws.Signatures[0]
|
sig := jws.Signatures[0]
|
||||||
|
if len(supportedSigAlgs) == 0 {
|
||||||
|
supportedSigAlgs = []string{"RS256"}
|
||||||
|
}
|
||||||
|
if !str.Contains(supportedSigAlgs, sig.Header.Algorithm) {
|
||||||
|
return fmt.Errorf("%w: id token signed with unsupported algorithm, expected %q got %q", ErrSignatureUnsupportedAlg, supportedSigAlgs, sig.Header.Algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
signedPayload, err := set.VerifySignature(ctx, jws)
|
signedPayload, err := set.VerifySignature(ctx, jws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -179,39 +166,27 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl
|
||||||
return nil
|
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 {
|
func CheckExpiration(claims Claims, offset time.Duration) error {
|
||||||
expiration := claims.GetExpiration()
|
expiration := claims.GetExpiration().Round(time.Second)
|
||||||
if !time.Now().Add(offset).Before(expiration) {
|
if !time.Now().UTC().Add(offset).Before(expiration) {
|
||||||
return ErrExpired
|
return ErrExpired
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckIssuedAt(claims Claims, maxAgeIAT, offset time.Duration) error {
|
func CheckIssuedAt(claims Claims, maxAgeIAT, offset time.Duration) error {
|
||||||
issuedAt := claims.GetIssuedAt()
|
issuedAt := claims.GetIssuedAt().Round(time.Second)
|
||||||
if issuedAt.IsZero() {
|
if issuedAt.IsZero() {
|
||||||
return ErrIatMissing
|
return ErrIatMissing
|
||||||
}
|
}
|
||||||
nowWithOffset := time.Now().Add(offset).Round(time.Second)
|
nowWithOffset := time.Now().UTC().Add(offset).Round(time.Second)
|
||||||
if issuedAt.After(nowWithOffset) {
|
if issuedAt.After(nowWithOffset) {
|
||||||
return fmt.Errorf("%w: (iat: %v, now with offset: %v)", ErrIatInFuture, issuedAt, nowWithOffset)
|
return fmt.Errorf("%w: (iat: %v, now with offset: %v)", ErrIatInFuture, issuedAt, nowWithOffset)
|
||||||
}
|
}
|
||||||
if maxAgeIAT == 0 {
|
if maxAgeIAT == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
maxAge := time.Now().Add(-maxAgeIAT).Round(time.Second)
|
maxAge := time.Now().UTC().Add(-maxAgeIAT).Round(time.Second)
|
||||||
if issuedAt.Before(maxAge) {
|
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 fmt.Errorf("%w: must not be older than %v, but was %v (%v to old)", ErrIatToOld, maxAge, issuedAt, maxAge.Sub(issuedAt))
|
||||||
}
|
}
|
||||||
|
@ -241,8 +216,8 @@ func CheckAuthTime(claims Claims, maxAge time.Duration) error {
|
||||||
if claims.GetAuthTime().IsZero() {
|
if claims.GetAuthTime().IsZero() {
|
||||||
return ErrAuthTimeNotPresent
|
return ErrAuthTimeNotPresent
|
||||||
}
|
}
|
||||||
authTime := claims.GetAuthTime()
|
authTime := claims.GetAuthTime().Round(time.Second)
|
||||||
maxAuthTime := time.Now().Add(-maxAge).Round(time.Second)
|
maxAuthTime := time.Now().UTC().Add(-maxAge).Round(time.Second)
|
||||||
if authTime.Before(maxAuthTime) {
|
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 fmt.Errorf("%w: must not be older than %v, but was %v (%v to old)", ErrAuthTimeToOld, maxAge, authTime, maxAuthTime.Sub(authTime))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,128 +0,0 @@
|
||||||
package oidc_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,374 +0,0 @@
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +1,20 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"log/slog"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
"github.com/gorilla/mux"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
"github.com/bmatcuk/doublestar/v4"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
str "github.com/zitadel/oidc/v2/pkg/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthRequest interface {
|
type AuthRequest interface {
|
||||||
|
@ -38,34 +35,20 @@ type AuthRequest interface {
|
||||||
Done() bool
|
Done() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthRequestSessionState should be implemented if [OpenID Connect Session Management](https://openid.net/specs/openid-connect-session-1_0.html) is supported
|
|
||||||
type AuthRequestSessionState interface {
|
|
||||||
// GetSessionState returns session_state.
|
|
||||||
// session_state is related to OpenID Connect Session Management.
|
|
||||||
GetSessionState() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Authorizer interface {
|
type Authorizer interface {
|
||||||
Storage() Storage
|
Storage() Storage
|
||||||
Decoder() httphelper.Decoder
|
Decoder() httphelper.Decoder
|
||||||
Encoder() httphelper.Encoder
|
Encoder() httphelper.Encoder
|
||||||
IDTokenHintVerifier(context.Context) *IDTokenHintVerifier
|
IDTokenHintVerifier(context.Context) IDTokenHintVerifier
|
||||||
Crypto() Crypto
|
Crypto() Crypto
|
||||||
RequestObjectSupported() bool
|
RequestObjectSupported() bool
|
||||||
Logger() *slog.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeValidator is an extension of Authorizer interface
|
// AuthorizeValidator is an extension of Authorizer interface
|
||||||
// implementing its own validation mechanism for the auth request
|
// implementing its own validation mechanism for the auth request
|
||||||
type AuthorizeValidator interface {
|
type AuthorizeValidator interface {
|
||||||
Authorizer
|
Authorizer
|
||||||
ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, *IDTokenHintVerifier) (string, error)
|
ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, IDTokenHintVerifier) (string, error)
|
||||||
}
|
|
||||||
|
|
||||||
type CodeResponseType struct {
|
|
||||||
Code string `schema:"code"`
|
|
||||||
State string `schema:"state,omitempty"`
|
|
||||||
SessionState string `schema:"session_state,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) {
|
func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) {
|
||||||
|
@ -74,7 +57,7 @@ func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Req
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AuthorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) {
|
func authorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
AuthorizeCallback(w, r, authorizer)
|
AuthorizeCallback(w, r, authorizer)
|
||||||
}
|
}
|
||||||
|
@ -83,54 +66,48 @@ func AuthorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, *
|
||||||
// Authorize handles the authorization request, including
|
// Authorize handles the authorization request, including
|
||||||
// parsing, validating, storing and finally redirecting to the login handler
|
// parsing, validating, storing and finally redirecting to the login handler
|
||||||
func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
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())
|
authReq, err := ParseAuthorizeRequest(r, authorizer.Decoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, nil, err, authorizer)
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx := r.Context()
|
||||||
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
|
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
|
||||||
err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
|
authReq, err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, nil, err, authorizer)
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if authReq.ClientID == "" {
|
if authReq.ClientID == "" {
|
||||||
AuthRequestError(w, r, nil, fmt.Errorf("auth request is missing client_id"), authorizer)
|
AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing client_id"), authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if authReq.RedirectURI == "" {
|
if authReq.RedirectURI == "" {
|
||||||
AuthRequestError(w, r, nil, fmt.Errorf("auth request is missing redirect_uri"), authorizer)
|
AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing redirect_uri"), authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
validation := ValidateAuthRequest
|
||||||
var client Client
|
if validater, ok := authorizer.(AuthorizeValidator); ok {
|
||||||
validation := func(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) {
|
validation = validater.ValidateAuthRequest
|
||||||
client, err = authorizer.Storage().GetClientByClientID(ctx, authReq.ClientID)
|
|
||||||
if err != nil {
|
|
||||||
return "", oidc.ErrInvalidRequestRedirectURI().WithDescription("unable to retrieve client by id").WithParent(err)
|
|
||||||
}
|
|
||||||
return ValidateAuthRequestClient(ctx, authReq, client, verifier)
|
|
||||||
}
|
|
||||||
if validator, ok := authorizer.(AuthorizeValidator); ok {
|
|
||||||
validation = validator.ValidateAuthRequest
|
|
||||||
}
|
}
|
||||||
userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx))
|
userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer)
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if authReq.RequestParam != "" {
|
if authReq.RequestParam != "" {
|
||||||
AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer)
|
AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req, err := authorizer.Storage().CreateAuthRequest(ctx, authReq, userID)
|
req, err := authorizer.Storage().CreateAuthRequest(ctx, authReq, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer)
|
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder())
|
||||||
|
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.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
RedirectToLogin(req.GetID(), client, w, r)
|
RedirectToLogin(req.GetID(), client, w, r)
|
||||||
|
@ -152,37 +129,37 @@ func ParseAuthorizeRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.A
|
||||||
|
|
||||||
// ParseRequestObject parse the `request` parameter, validates the token including the signature
|
// ParseRequestObject parse the `request` parameter, validates the token including the signature
|
||||||
// and copies the token claims into the auth request
|
// and copies the token claims into the auth request
|
||||||
func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, issuer string) error {
|
func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, issuer string) (*oidc.AuthRequest, error) {
|
||||||
requestObject := new(oidc.RequestObject)
|
requestObject := new(oidc.RequestObject)
|
||||||
payload, err := oidc.ParseToken(authReq.RequestParam, requestObject)
|
payload, err := oidc.ParseToken(authReq.RequestParam, requestObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if requestObject.ClientID != "" && requestObject.ClientID != authReq.ClientID {
|
if requestObject.ClientID != "" && requestObject.ClientID != authReq.ClientID {
|
||||||
return oidc.ErrInvalidRequest().WithDescription("missing or wrong client id in request")
|
return authReq, oidc.ErrInvalidRequest()
|
||||||
}
|
}
|
||||||
if requestObject.ResponseType != "" && requestObject.ResponseType != authReq.ResponseType {
|
if requestObject.ResponseType != "" && requestObject.ResponseType != authReq.ResponseType {
|
||||||
return oidc.ErrInvalidRequest().WithDescription("missing or wrong response type in request")
|
return authReq, oidc.ErrInvalidRequest()
|
||||||
}
|
}
|
||||||
if requestObject.Issuer != requestObject.ClientID {
|
if requestObject.Issuer != requestObject.ClientID {
|
||||||
return oidc.ErrInvalidRequest().WithDescription("missing or wrong issuer in request")
|
return authReq, oidc.ErrInvalidRequest()
|
||||||
}
|
}
|
||||||
if !slices.Contains(requestObject.Audience, issuer) {
|
if !str.Contains(requestObject.Audience, issuer) {
|
||||||
return oidc.ErrInvalidRequest().WithDescription("issuer missing in audience")
|
return authReq, oidc.ErrInvalidRequest()
|
||||||
}
|
}
|
||||||
keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer}
|
keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer}
|
||||||
if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil {
|
if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil {
|
||||||
return oidc.ErrInvalidRequest().WithParent(err).WithDescription(err.Error())
|
return authReq, err
|
||||||
}
|
}
|
||||||
CopyRequestObjectToAuthRequest(authReq, requestObject)
|
CopyRequestObjectToAuthRequest(authReq, requestObject)
|
||||||
return nil
|
return authReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request
|
// CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request
|
||||||
// and clears the `RequestParam` of the auth request
|
// and clears the `RequestParam` of the auth request
|
||||||
func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) {
|
func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) {
|
||||||
if slices.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 {
|
if str.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 {
|
||||||
authReq.Scopes = requestObject.Scopes
|
authReq.Scopes = requestObject.Scopes
|
||||||
}
|
}
|
||||||
if requestObject.RedirectURI != "" {
|
if requestObject.RedirectURI != "" {
|
||||||
|
@ -227,37 +204,23 @@ func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oi
|
||||||
authReq.RequestParam = ""
|
authReq.RequestParam = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed.
|
// 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) {
|
||||||
// Deprecated: Use [ValidateAuthRequestClient] to prevent querying for the Client twice.
|
|
||||||
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()
|
|
||||||
|
|
||||||
client, err := storage.GetClientByClientID(ctx, authReq.ClientID)
|
|
||||||
if err != nil {
|
|
||||||
return "", oidc.ErrInvalidRequestRedirectURI().WithDescription("unable to retrieve client by id").WithParent(err)
|
|
||||||
}
|
|
||||||
return ValidateAuthRequestClient(ctx, authReq, client, verifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAuthRequestClient validates the Auth request against the passed client.
|
|
||||||
// If id_token_hint is part of the request, the subject of the token is returned.
|
|
||||||
func ValidateAuthRequestClient(ctx context.Context, authReq *oidc.AuthRequest, client Client, verifier *IDTokenHintVerifier) (sub string, err error) {
|
|
||||||
ctx, span := tracer.Start(ctx, "ValidateAuthRequestClient")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge)
|
authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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)
|
authReq.Scopes, err = ValidateAuthReqScopes(client, authReq.Scopes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if err := ValidateAuthReqResponseType(client, authReq.ResponseType); err != nil {
|
if err := ValidateAuthReqResponseType(client, authReq.ResponseType); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -277,35 +240,49 @@ func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error)
|
||||||
return maxAge, nil
|
return maxAge, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAuthReqScopes validates the passed scopes and deletes any unsupported scopes.
|
// ValidateAuthReqScopes validates the passed scopes
|
||||||
// An error is returned if scopes is empty.
|
|
||||||
func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) {
|
func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) {
|
||||||
if len(scopes) == 0 {
|
if len(scopes) == 0 {
|
||||||
return nil, oidc.ErrInvalidRequest().
|
return nil, oidc.ErrInvalidRequest().
|
||||||
WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " +
|
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.")
|
"If you have any questions, you may contact the administrator of the application.")
|
||||||
}
|
}
|
||||||
scopes = slices.DeleteFunc(scopes, func(scope string) bool {
|
openID := false
|
||||||
return !(scope == oidc.ScopeOpenID ||
|
for i := len(scopes) - 1; i >= 0; i-- {
|
||||||
scope == oidc.ScopeProfile ||
|
scope := scopes[i]
|
||||||
|
if scope == oidc.ScopeOpenID {
|
||||||
|
openID = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !(scope == oidc.ScopeProfile ||
|
||||||
scope == oidc.ScopeEmail ||
|
scope == oidc.ScopeEmail ||
|
||||||
scope == oidc.ScopePhone ||
|
scope == oidc.ScopePhone ||
|
||||||
scope == oidc.ScopeAddress ||
|
scope == oidc.ScopeAddress ||
|
||||||
scope == oidc.ScopeOfflineAccess) &&
|
scope == oidc.ScopeOfflineAccess) &&
|
||||||
!client.IsScopeAllowed(scope)
|
!client.IsScopeAllowed(scope) {
|
||||||
})
|
scopes[i] = scopes[len(scopes)-1]
|
||||||
|
scopes[len(scopes)-1] = ""
|
||||||
|
scopes = scopes[:len(scopes)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !openID {
|
||||||
|
return nil, oidc.ErrInvalidScope().WithDescription("The scope openid is missing in your request. " +
|
||||||
|
"Please ensure the scope openid is added to the request. " +
|
||||||
|
"If you have any questions, you may contact the administrator of the application.")
|
||||||
|
}
|
||||||
|
|
||||||
return scopes, nil
|
return scopes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores
|
// checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores
|
||||||
// other factors.
|
// other factors.
|
||||||
func checkURIAgainstRedirects(client Client, uri string) error {
|
func checkURIAgainstRedirects(client Client, uri string) error {
|
||||||
if slices.Contains(client.RedirectURIs(), uri) {
|
if str.Contains(client.RedirectURIs(), uri) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if globClient, ok := client.(HasRedirectGlobs); ok {
|
if globClient, ok := client.(HasRedirectGlobs); ok {
|
||||||
for _, uriGlob := range globClient.RedirectURIGlobs() {
|
for _, uriGlob := range globClient.RedirectURIGlobs() {
|
||||||
isMatch, err := doublestar.Match(uriGlob, uri)
|
isMatch, err := path.Match(uriGlob, uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return oidc.ErrServerError().WithParent(err)
|
return oidc.ErrServerError().WithParent(err)
|
||||||
}
|
}
|
||||||
|
@ -325,12 +302,12 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res
|
||||||
return oidc.ErrInvalidRequestRedirectURI().WithDescription("The redirect_uri is missing in the request. " +
|
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.")
|
"Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.")
|
||||||
}
|
}
|
||||||
if client.ApplicationType() == ApplicationTypeNative {
|
|
||||||
return validateAuthReqRedirectURINative(client, uri)
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(uri, "https://") {
|
if strings.HasPrefix(uri, "https://") {
|
||||||
return checkURIAgainstRedirects(client, uri)
|
return checkURIAgainstRedirects(client, uri)
|
||||||
}
|
}
|
||||||
|
if client.ApplicationType() == ApplicationTypeNative {
|
||||||
|
return validateAuthReqRedirectURINative(client, uri, responseType)
|
||||||
|
}
|
||||||
if err := checkURIAgainstRedirects(client, uri); err != nil {
|
if err := checkURIAgainstRedirects(client, uri); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -349,17 +326,14 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type
|
// ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type
|
||||||
func validateAuthReqRedirectURINative(client Client, uri string) error {
|
func validateAuthReqRedirectURINative(client Client, uri string, responseType oidc.ResponseType) error {
|
||||||
parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri)
|
parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri)
|
||||||
isCustomSchema := !(strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://"))
|
isCustomSchema := !strings.HasPrefix(uri, "http://")
|
||||||
if err := checkURIAgainstRedirects(client, uri); err == nil {
|
if err := checkURIAgainstRedirects(client, uri); err == nil {
|
||||||
if client.DevMode() {
|
if client.DevMode() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !isLoopback && strings.HasPrefix(uri, "https://") {
|
// The RedirectURIs are only valid for native clients when localhost or non-"http://"
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// The RedirectURIs are only valid for native clients when localhost or non-"http://" and "https://"
|
|
||||||
if isLoopback || isCustomSchema {
|
if isLoopback || isCustomSchema {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -384,16 +358,16 @@ func equalURI(url1, url2 *url.URL) bool {
|
||||||
return url1.Path == url2.Path && url1.RawQuery == url2.RawQuery
|
return url1.Path == url2.Path && url1.RawQuery == url2.RawQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
func HTTPLoopbackOrLocalhost(rawURL string) (*url.URL, bool) {
|
func HTTPLoopbackOrLocalhost(rawurl string) (*url.URL, bool) {
|
||||||
parsedURL, err := url.Parse(rawURL)
|
parsedURL, err := url.Parse(rawurl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
if parsedURL.Scheme == "http" || parsedURL.Scheme == "https" {
|
if parsedURL.Scheme != "http" {
|
||||||
hostName := parsedURL.Hostname()
|
return nil, false
|
||||||
return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback()
|
|
||||||
}
|
}
|
||||||
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
|
// ValidateAuthReqResponseType validates the passed response_type to the registered response types
|
||||||
|
@ -411,14 +385,14 @@ func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType)
|
||||||
|
|
||||||
// ValidateAuthReqIDTokenHint validates the id_token_hint (if passed as parameter in the request)
|
// ValidateAuthReqIDTokenHint validates the id_token_hint (if passed as parameter in the request)
|
||||||
// and returns the `sub` claim
|
// and returns the `sub` claim
|
||||||
func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifier *IDTokenHintVerifier) (string, error) {
|
func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifier IDTokenHintVerifier) (string, error) {
|
||||||
if idTokenHint == "" {
|
if idTokenHint == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, idTokenHint, verifier)
|
claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, idTokenHint, verifier)
|
||||||
if err != nil && !errors.As(err, &IDTokenHintExpiredError{}) {
|
if err != nil {
|
||||||
return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " +
|
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)
|
"If you have any questions, you may contact the administrator of the application.")
|
||||||
}
|
}
|
||||||
return claims.GetSubject(), nil
|
return claims.GetSubject(), nil
|
||||||
}
|
}
|
||||||
|
@ -431,49 +405,32 @@ func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r *
|
||||||
|
|
||||||
// AuthorizeCallback handles the callback after authentication in the Login UI
|
// AuthorizeCallback handles the callback after authentication in the Login UI
|
||||||
func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||||
ctx, span := tracer.Start(r.Context(), "AuthorizeCallback")
|
params := mux.Vars(r)
|
||||||
r = r.WithContext(ctx)
|
id := params["id"]
|
||||||
defer span.End()
|
if id == "" {
|
||||||
|
AuthRequestError(w, r, nil, fmt.Errorf("auth request callback is missing id"), authorizer.Encoder())
|
||||||
id, err := ParseAuthorizeCallbackRequest(r)
|
|
||||||
if err != nil {
|
|
||||||
AuthRequestError(w, r, nil, err, authorizer)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authReq, err := authorizer.Storage().AuthRequestByID(r.Context(), id)
|
authReq, err := authorizer.Storage().AuthRequestByID(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, nil, err, authorizer)
|
AuthRequestError(w, r, nil, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !authReq.Done() {
|
if !authReq.Done() {
|
||||||
AuthRequestError(w, r, authReq,
|
AuthRequestError(w, r, authReq,
|
||||||
oidc.ErrInteractionRequired().WithDescription("Unfortunately, the user may be not logged in and/or additional interaction is required."),
|
oidc.ErrInteractionRequired().WithDescription("Unfortunately, the user may be not logged in and/or additional interaction is required."),
|
||||||
authorizer)
|
authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
AuthResponse(authReq, authorizer, w, r)
|
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)
|
// AuthResponse creates the successful authentication response (either code or tokens)
|
||||||
func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) {
|
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())
|
client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer)
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if authReq.GetResponseType() == oidc.ResponseTypeCode {
|
if authReq.GetResponseType() == oidc.ResponseTypeCode {
|
||||||
|
@ -483,98 +440,39 @@ func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWri
|
||||||
AuthResponseToken(w, r, authReq, authorizer, client)
|
AuthResponseToken(w, r, authReq, authorizer, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthResponseCode handles the creation of a successful authentication response using an authorization code
|
// AuthResponseCode creates the successful code authentication response
|
||||||
func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) {
|
func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) {
|
||||||
ctx, span := tracer.Start(r.Context(), "AuthResponseCode")
|
code, err := CreateAuthRequestCode(r.Context(), authReq, authorizer.Storage(), authorizer.Crypto())
|
||||||
defer span.End()
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if authReq.GetResponseMode() == oidc.ResponseModeFormPost {
|
|
||||||
err = handleFormPostResponse(w, r, authReq, authorizer)
|
|
||||||
} else {
|
|
||||||
err = handleRedirectResponse(w, r, authReq, authorizer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer)
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
codeResponse := struct {
|
||||||
|
code string
|
||||||
// handleFormPostResponse processes the authentication response using form post method
|
state string
|
||||||
func handleFormPostResponse(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) error {
|
}{
|
||||||
codeResponse, err := BuildAuthResponseCodeResponsePayload(r.Context(), authReq, authorizer)
|
code: code,
|
||||||
|
state: authReq.GetState(),
|
||||||
|
}
|
||||||
|
callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return AuthResponseFormPost(w, authReq.GetRedirectURI(), codeResponse, authorizer.Encoder())
|
http.Redirect(w, r, callback, http.StatusFound)
|
||||||
}
|
|
||||||
|
|
||||||
// handleRedirectResponse processes the authentication response using the redirect method
|
|
||||||
func handleRedirectResponse(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) error {
|
|
||||||
callbackURL, err := BuildAuthResponseCallbackURL(r.Context(), authReq, authorizer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, callbackURL, http.StatusFound)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildAuthResponseCodeResponsePayload generates the authorization code response payload for the authentication request
|
|
||||||
func BuildAuthResponseCodeResponsePayload(ctx context.Context, authReq AuthRequest, authorizer Authorizer) (*CodeResponseType, error) {
|
|
||||||
code, err := CreateAuthRequestCode(ctx, authReq, authorizer.Storage(), authorizer.Crypto())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionState := ""
|
|
||||||
if authRequestSessionState, ok := authReq.(AuthRequestSessionState); ok {
|
|
||||||
sessionState = authRequestSessionState.GetSessionState()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CodeResponseType{
|
|
||||||
Code: code,
|
|
||||||
State: authReq.GetState(),
|
|
||||||
SessionState: sessionState,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildAuthResponseCallbackURL generates the callback URL for a successful authorization code response
|
|
||||||
func BuildAuthResponseCallbackURL(ctx context.Context, authReq AuthRequest, authorizer Authorizer) (string, error) {
|
|
||||||
codeResponse, err := BuildAuthResponseCodeResponsePayload(ctx, authReq, authorizer)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), codeResponse, authorizer.Encoder())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthResponseToken creates the successful token(s) authentication response
|
// AuthResponseToken creates the successful token(s) authentication response
|
||||||
func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer, client Client) {
|
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
|
createAccessToken := authReq.GetResponseType() != oidc.ResponseTypeIDTokenOnly
|
||||||
resp, err := CreateTokenResponse(r.Context(), authReq, client, authorizer, createAccessToken, "", "")
|
resp, err := CreateTokenResponse(r.Context(), authReq, client, authorizer, createAccessToken, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer)
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
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())
|
callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), resp, authorizer.Encoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer)
|
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, callback, http.StatusFound)
|
http.Redirect(w, r, callback, http.StatusFound)
|
||||||
|
@ -582,9 +480,6 @@ func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthReque
|
||||||
|
|
||||||
// CreateAuthRequestCode creates and stores a code for the auth code response
|
// CreateAuthRequestCode creates and stores a code for the auth code response
|
||||||
func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Storage, crypto Crypto) (string, error) {
|
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)
|
code, err := BuildAuthRequestCode(authReq, crypto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -602,7 +497,7 @@ func BuildAuthRequestCode(authReq AuthRequest, crypto Crypto) (string, error) {
|
||||||
|
|
||||||
// AuthResponseURL encodes the authorization response (successful and error) and sets it as query or fragment values
|
// AuthResponseURL encodes the authorization response (successful and error) and sets it as query or fragment values
|
||||||
// depending on the response_mode and response_type
|
// 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) {
|
func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, responseMode oidc.ResponseMode, response interface{}, encoder httphelper.Encoder) (string, error) {
|
||||||
uri, err := url.Parse(redirectURI)
|
uri, err := url.Parse(redirectURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", oidc.ErrServerError().WithParent(err)
|
return "", oidc.ErrServerError().WithParent(err)
|
||||||
|
@ -626,43 +521,6 @@ func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, respons
|
||||||
return mergeQueryParams(uri, params), nil
|
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 {
|
func setFragment(uri *url.URL, params url.Values) string {
|
||||||
uri.Fragment = params.Encode()
|
uri.Fragment = params.Encode()
|
||||||
return uri.String()
|
return uri.String()
|
||||||
|
|
|
@ -3,54 +3,76 @@ package op_test
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
|
"github.com/gorilla/schema"
|
||||||
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
|
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op/mock"
|
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/zitadel/schema"
|
|
||||||
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAuthorize(t *testing.T) {
|
//
|
||||||
tests := []struct {
|
// TOOD: tests will be implemented in branch for service accounts
|
||||||
name string
|
// func TestAuthorize(t *testing.T) {
|
||||||
req *http.Request
|
// // testCallback := func(t *testing.T, clienID string) callbackHandler {
|
||||||
expect func(a *mock.MockAuthorizerMockRecorder)
|
// // return func(authReq *oidc.AuthRequest, client oidc.Client, w http.ResponseWriter, r *http.Request) {
|
||||||
}{
|
// // // require.Equal(t, clientID, client.)
|
||||||
{
|
// // }
|
||||||
name: "parse error", // used to panic, see issue #315
|
// // }
|
||||||
req: httptest.NewRequest(http.MethodPost, "/?;", nil),
|
// // testErr := func(t *testing.T, expected error) errorHandler {
|
||||||
},
|
// // return func(w http.ResponseWriter, r *http.Request, authReq *oidc.AuthRequest, err error) {
|
||||||
}
|
// // require.Equal(t, expected, err)
|
||||||
for _, tt := range tests {
|
// // }
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
// // }
|
||||||
w := httptest.NewRecorder()
|
// type args struct {
|
||||||
authorizer := mock.NewMockAuthorizer(gomock.NewController(t))
|
// w http.ResponseWriter
|
||||||
|
// r *http.Request
|
||||||
expect := authorizer.EXPECT()
|
// authorizer op.Authorizer
|
||||||
expect.Decoder().Return(schema.NewDecoder())
|
// }
|
||||||
expect.Logger().Return(slog.Default())
|
// tests := []struct {
|
||||||
|
// name string
|
||||||
if tt.expect != nil {
|
// args args
|
||||||
tt.expect(expect)
|
// }{
|
||||||
}
|
// {
|
||||||
|
// "parsing fails",
|
||||||
op.Authorize(w, tt.req, authorizer)
|
// args{
|
||||||
})
|
// httptest.NewRecorder(),
|
||||||
}
|
// &http.Request{Method: "POST", Body: nil},
|
||||||
}
|
// mock.NewAuthorizerExpectValid(t, true),
|
||||||
|
// // testCallback(t, ""),
|
||||||
|
// // testErr(t, ErrInvalidRequest("cannot parse form")),
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "decoding fails",
|
||||||
|
// args{
|
||||||
|
// httptest.NewRecorder(),
|
||||||
|
// func() *http.Request {
|
||||||
|
// r := httptest.NewRequest("POST", "/authorize", strings.NewReader("client_id=foo"))
|
||||||
|
// r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
// return r
|
||||||
|
// }(),
|
||||||
|
// mock.NewAuthorizerExpectValid(t, true),
|
||||||
|
// // testCallback(t, ""),
|
||||||
|
// // testErr(t, ErrInvalidRequest("cannot parse auth request")),
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// // {"decoding fails", args{httptest.NewRecorder(), &http.Request{}, mock.NewAuthorizerExpectValid(t), nil, testErr(t, nil)}},
|
||||||
|
// }
|
||||||
|
// for _, tt := range tests {
|
||||||
|
// t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// op.Authorize(tt.args.w, tt.args.r, tt.args.authorizer)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
func TestParseAuthorizeRequest(t *testing.T) {
|
func TestParseAuthorizeRequest(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
|
@ -125,7 +147,7 @@ func TestValidateAuthRequest(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
authRequest *oidc.AuthRequest
|
authRequest *oidc.AuthRequest
|
||||||
storage op.Storage
|
storage op.Storage
|
||||||
verifier *op.IDTokenHintVerifier
|
verifier op.IDTokenHintVerifier
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -137,6 +159,11 @@ func TestValidateAuthRequest(t *testing.T) {
|
||||||
args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil},
|
args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil},
|
||||||
oidc.ErrInvalidRequest(),
|
oidc.ErrInvalidRequest(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"scope openid missing fails",
|
||||||
|
args{&oidc.AuthRequest{Scopes: []string{"profile"}}, mock.NewMockStorageExpectValidClientID(t), nil},
|
||||||
|
oidc.ErrInvalidScope(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"response_type missing fails",
|
"response_type missing fails",
|
||||||
args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil},
|
args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil},
|
||||||
|
@ -282,6 +309,16 @@ func TestValidateAuthReqScopes(t *testing.T) {
|
||||||
err: true,
|
err: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"scope openid missing fails",
|
||||||
|
args{
|
||||||
|
mock.NewClientExpectAny(t, op.ApplicationTypeWeb),
|
||||||
|
[]string{"email"},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"scope ok",
|
"scope ok",
|
||||||
args{
|
args{
|
||||||
|
@ -433,24 +470,6 @@ func TestValidateAuthReqRedirectURI(t *testing.T) {
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"code flow registered https loopback v4 native ok",
|
|
||||||
args{
|
|
||||||
"https://127.0.0.1:4200/callback",
|
|
||||||
mock.NewClientWithConfig(t, []string{"https://127.0.0.1/callback"}, op.ApplicationTypeNative, nil, false),
|
|
||||||
oidc.ResponseTypeCode,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code flow registered https loopback v6 native ok",
|
|
||||||
args{
|
|
||||||
"https://[::1]:4200/callback",
|
|
||||||
mock.NewClientWithConfig(t, []string{"https://[::1]/callback"}, op.ApplicationTypeNative, nil, false),
|
|
||||||
oidc.ResponseTypeCode,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"code flow unregistered http native fails",
|
"code flow unregistered http native fails",
|
||||||
args{
|
args{
|
||||||
|
@ -586,60 +605,6 @@ func TestValidateAuthReqRedirectURI(t *testing.T) {
|
||||||
},
|
},
|
||||||
false,
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -804,7 +769,7 @@ func TestAuthResponseURL(t *testing.T) {
|
||||||
redirectURI string
|
redirectURI string
|
||||||
responseType oidc.ResponseType
|
responseType oidc.ResponseType
|
||||||
responseMode oidc.ResponseMode
|
responseMode oidc.ResponseMode
|
||||||
response any
|
response interface{}
|
||||||
encoder httphelper.Encoder
|
encoder httphelper.Encoder
|
||||||
}
|
}
|
||||||
type res struct {
|
type res struct {
|
||||||
|
@ -822,7 +787,7 @@ func TestAuthResponseURL(t *testing.T) {
|
||||||
"uri",
|
"uri",
|
||||||
oidc.ResponseTypeCode,
|
oidc.ResponseTypeCode,
|
||||||
"",
|
"",
|
||||||
map[string]any{"test": "test"},
|
map[string]interface{}{"test": "test"},
|
||||||
&mockEncoder{
|
&mockEncoder{
|
||||||
errors.New("error encoding"),
|
errors.New("error encoding"),
|
||||||
},
|
},
|
||||||
|
@ -993,7 +958,7 @@ type mockEncoder struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockEncoder) Encode(src any, dst map[string][]string) error {
|
func (m *mockEncoder) Encode(src interface{}, dst map[string][]string) error {
|
||||||
if m.err != nil {
|
if m.err != nil {
|
||||||
return m.err
|
return m.err
|
||||||
}
|
}
|
||||||
|
@ -1002,611 +967,3 @@ func (m *mockEncoder) Encode(src any, dst map[string][]string) error {
|
||||||
}
|
}
|
||||||
return nil
|
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 with state and session_state",
|
|
||||||
args: args{
|
|
||||||
authReq: &storage.AuthRequestWithSessionState{
|
|
||||||
AuthRequest: &storage.AuthRequest{
|
|
||||||
ID: "id1",
|
|
||||||
TransferState: "state1",
|
|
||||||
},
|
|
||||||
SessionState: "session_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&session_state=session_state1&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: "<!doctype html>\n<html>\n<head><meta charset=\"UTF-8\" /></head>\n<body onload=\"javascript:document.forms[0].submit()\">\n<form method=\"post\" action=\"https://example.com/callback\">\n<input type=\"hidden\" name=\"state\" value=\"state1\"/>\n<input type=\"hidden\" name=\"code\" value=\"id1\" />\n\n\n\n\n</form>\n</body>\n</html>",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
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 TestBuildAuthResponseCodeResponsePayload(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
authReq op.AuthRequest
|
|
||||||
authorizer func(*testing.T) op.Authorizer
|
|
||||||
}
|
|
||||||
type res struct {
|
|
||||||
wantCode string
|
|
||||||
wantState string
|
|
||||||
wantSessionState string
|
|
||||||
wantErr bool
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
res res
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "create code error",
|
|
||||||
args: args{
|
|
||||||
authReq: &storage.AuthRequest{
|
|
||||||
ID: "id1",
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
return authorizer
|
|
||||||
},
|
|
||||||
},
|
|
||||||
res: res{
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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{})
|
|
||||||
return authorizer
|
|
||||||
},
|
|
||||||
},
|
|
||||||
res: res{
|
|
||||||
wantCode: "id1",
|
|
||||||
wantState: "state1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success without state",
|
|
||||||
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{})
|
|
||||||
return authorizer
|
|
||||||
},
|
|
||||||
},
|
|
||||||
res: res{
|
|
||||||
wantCode: "id1",
|
|
||||||
wantState: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with session_state",
|
|
||||||
args: args{
|
|
||||||
authReq: &storage.AuthRequestWithSessionState{
|
|
||||||
AuthRequest: &storage.AuthRequest{
|
|
||||||
ID: "id1",
|
|
||||||
TransferState: "state1",
|
|
||||||
},
|
|
||||||
SessionState: "session_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{})
|
|
||||||
return authorizer
|
|
||||||
},
|
|
||||||
},
|
|
||||||
res: res{
|
|
||||||
wantCode: "id1",
|
|
||||||
wantState: "state1",
|
|
||||||
wantSessionState: "session_state1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := op.BuildAuthResponseCodeResponsePayload(context.Background(), tt.args.authReq, tt.args.authorizer(t))
|
|
||||||
if tt.res.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.res.wantCode, got.Code)
|
|
||||||
assert.Equal(t, tt.res.wantState, got.State)
|
|
||||||
assert.Equal(t, tt.res.wantSessionState, got.SessionState)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildAuthResponseCallbackURL(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
authReq op.AuthRequest
|
|
||||||
authorizer func(*testing.T) op.Authorizer
|
|
||||||
}
|
|
||||||
type res struct {
|
|
||||||
wantURL string
|
|
||||||
wantErr bool
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
res res
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "error when generating code response",
|
|
||||||
args: args{
|
|
||||||
authReq: &storage.AuthRequest{
|
|
||||||
ID: "id1",
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
return authorizer
|
|
||||||
},
|
|
||||||
},
|
|
||||||
res: res{
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error when generating callback URL",
|
|
||||||
args: args{
|
|
||||||
authReq: &storage.AuthRequest{
|
|
||||||
ID: "id1",
|
|
||||||
CallbackURI: "://invalid-url",
|
|
||||||
},
|
|
||||||
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{
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with state",
|
|
||||||
args: args{
|
|
||||||
authReq: &storage.AuthRequest{
|
|
||||||
ID: "id1",
|
|
||||||
CallbackURI: "https://example.com/callback",
|
|
||||||
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{
|
|
||||||
wantURL: "https://example.com/callback?code=id1&state=state1",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success without state",
|
|
||||||
args: args{
|
|
||||||
authReq: &storage.AuthRequest{
|
|
||||||
ID: "id1",
|
|
||||||
CallbackURI: "https://example.com/callback",
|
|
||||||
},
|
|
||||||
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{
|
|
||||||
wantURL: "https://example.com/callback?code=id1",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with session_state",
|
|
||||||
args: args{
|
|
||||||
authReq: &storage.AuthRequestWithSessionState{
|
|
||||||
AuthRequest: &storage.AuthRequest{
|
|
||||||
ID: "id1",
|
|
||||||
CallbackURI: "https://example.com/callback",
|
|
||||||
TransferState: "state1",
|
|
||||||
},
|
|
||||||
SessionState: "session_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{
|
|
||||||
wantURL: "https://example.com/callback?code=id1&session_state=session_state1&state=state1",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with existing query parameters",
|
|
||||||
args: args{
|
|
||||||
authReq: &storage.AuthRequest{
|
|
||||||
ID: "id1",
|
|
||||||
CallbackURI: "https://example.com/callback?param=value",
|
|
||||||
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{
|
|
||||||
wantURL: "https://example.com/callback?param=value&code=id1&state=state1",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with fragment response mode",
|
|
||||||
args: args{
|
|
||||||
authReq: &storage.AuthRequest{
|
|
||||||
ID: "id1",
|
|
||||||
CallbackURI: "https://example.com/callback",
|
|
||||||
TransferState: "state1",
|
|
||||||
ResponseMode: "fragment",
|
|
||||||
},
|
|
||||||
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{
|
|
||||||
wantURL: "https://example.com/callback#code=id1&state=state1",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := op.BuildAuthResponseCallbackURL(context.Background(), tt.args.authReq, tt.args.authorizer(t))
|
|
||||||
if tt.res.wantErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
if tt.res.wantURL != "" {
|
|
||||||
// Parse the URLs to compare components instead of direct string comparison
|
|
||||||
expectedURL, err := url.Parse(tt.res.wantURL)
|
|
||||||
require.NoError(t, err)
|
|
||||||
actualURL, err := url.Parse(got)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Compare the base parts (scheme, host, path)
|
|
||||||
assert.Equal(t, expectedURL.Scheme, actualURL.Scheme)
|
|
||||||
assert.Equal(t, expectedURL.Host, actualURL.Host)
|
|
||||||
assert.Equal(t, expectedURL.Path, actualURL.Path)
|
|
||||||
|
|
||||||
// Compare the fragment if any
|
|
||||||
assert.Equal(t, expectedURL.Fragment, actualURL.Fragment)
|
|
||||||
|
|
||||||
// For query parameters, compare them independently of order
|
|
||||||
expectedQuery := expectedURL.Query()
|
|
||||||
actualQuery := actualURL.Query()
|
|
||||||
|
|
||||||
assert.Equal(t, len(expectedQuery), len(actualQuery), "Query parameter count does not match")
|
|
||||||
|
|
||||||
for key, expectedValues := range expectedQuery {
|
|
||||||
actualValues, exists := actualQuery[key]
|
|
||||||
assert.True(t, exists, "Expected query parameter %s not found", key)
|
|
||||||
assert.ElementsMatch(t, expectedValues, actualValues, "Values for parameter %s don't match", key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate go get github.com/dmarkham/enumer
|
//go:generate go get github.com/dmarkham/enumer
|
||||||
|
@ -63,7 +63,6 @@ type Client interface {
|
||||||
// such as DevMode for the client being enabled.
|
// such as DevMode for the client being enabled.
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||||
type HasRedirectGlobs interface {
|
type HasRedirectGlobs interface {
|
||||||
Client
|
|
||||||
RedirectURIGlobs() []string
|
RedirectURIGlobs() []string
|
||||||
PostLogoutRedirectURIGlobs() []string
|
PostLogoutRedirectURIGlobs() []string
|
||||||
}
|
}
|
||||||
|
@ -88,13 +87,10 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClientJWTProfile interface {
|
type ClientJWTProfile interface {
|
||||||
JWTProfileVerifier(context.Context) *JWTProfileVerifier
|
JWTProfileVerifier(context.Context) JWTProfileVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) {
|
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 == "" {
|
if ca.ClientAssertion == "" {
|
||||||
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
|
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
|
||||||
}
|
}
|
||||||
|
@ -107,10 +103,6 @@ func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClientBasicAuth(r *http.Request, storage Storage) (clientID string, err error) {
|
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()
|
clientID, clientSecret, ok := r.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
|
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
|
||||||
|
@ -158,44 +150,24 @@ func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, au
|
||||||
return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err)
|
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)
|
data := new(clientData)
|
||||||
if err = p.Decoder().Decode(data, r.Form); err != nil {
|
if err = p.Decoder().Decode(data, r.PostForm); err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
JWTProfile, ok := p.(ClientJWTProfile)
|
JWTProfile, ok := p.(ClientJWTProfile)
|
||||||
if ok && data.ClientAssertion != "" {
|
if ok {
|
||||||
// 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)
|
clientID, err = ClientJWTAuth(r.Context(), data.ClientAssertionParams, JWTProfile)
|
||||||
return clientID, err == nil, err
|
|
||||||
}
|
}
|
||||||
// try basic auth
|
if !ok || errors.Is(err, ErrNoClientCredentials) {
|
||||||
clientID, err = ClientBasicAuth(r, p.Storage())
|
clientID, err = ClientBasicAuth(r, p.Storage())
|
||||||
// if that succeeded, use it
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return clientID, true, 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 == "" {
|
if data.ClientID == "" {
|
||||||
return "", false, oidc.ErrInvalidClient().WithParent(ErrMissingClientID)
|
return "", false, oidc.ErrInvalidClient().WithParent(ErrMissingClientID)
|
||||||
}
|
}
|
||||||
return data.ClientID, false, nil
|
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"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,19 +10,19 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op/mock"
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/gorilla/schema"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/zitadel/schema"
|
httphelper "github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testClientJWTProfile struct{}
|
type testClientJWTProfile struct{}
|
||||||
|
|
||||||
func (testClientJWTProfile) JWTProfileVerifier(context.Context) *op.JWTProfileVerifier { return nil }
|
func (testClientJWTProfile) JWTProfileVerifier(context.Context) op.JWTProfileVerifier { return nil }
|
||||||
|
|
||||||
func TestClientJWTAuth(t *testing.T) {
|
func TestClientJWTAuth(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
|
@ -108,7 +108,7 @@ func TestClientBasicAuth(t *testing.T) {
|
||||||
},
|
},
|
||||||
storage: func() op.Storage {
|
storage: func() op.Storage {
|
||||||
s := mock.NewMockStorage(gomock.NewController(t))
|
s := mock.NewMockStorage(gomock.NewController(t))
|
||||||
s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "wrong").Return(errWrong)
|
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "wrong").Return(errWrong)
|
||||||
return s
|
return s
|
||||||
}(),
|
}(),
|
||||||
wantErr: errWrong,
|
wantErr: errWrong,
|
||||||
|
@ -121,7 +121,7 @@ func TestClientBasicAuth(t *testing.T) {
|
||||||
},
|
},
|
||||||
storage: func() op.Storage {
|
storage: func() op.Storage {
|
||||||
s := mock.NewMockStorage(gomock.NewController(t))
|
s := mock.NewMockStorage(gomock.NewController(t))
|
||||||
s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "bar").Return(nil)
|
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil)
|
||||||
return s
|
return s
|
||||||
}(),
|
}(),
|
||||||
wantClientID: "foo",
|
wantClientID: "foo",
|
||||||
|
@ -207,7 +207,7 @@ func TestClientIDFromRequest(t *testing.T) {
|
||||||
p: testClientProvider{
|
p: testClientProvider{
|
||||||
storage: func() op.Storage {
|
storage: func() op.Storage {
|
||||||
s := mock.NewMockStorage(gomock.NewController(t))
|
s := mock.NewMockStorage(gomock.NewController(t))
|
||||||
s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "bar").Return(nil)
|
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil)
|
||||||
return s
|
return s
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,12 +2,10 @@ package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/muhlemmer/httpforwarded"
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,15 +20,14 @@ var (
|
||||||
type Configuration interface {
|
type Configuration interface {
|
||||||
IssuerFromRequest(r *http.Request) string
|
IssuerFromRequest(r *http.Request) string
|
||||||
Insecure() bool
|
Insecure() bool
|
||||||
AuthorizationEndpoint() *Endpoint
|
AuthorizationEndpoint() Endpoint
|
||||||
TokenEndpoint() *Endpoint
|
TokenEndpoint() Endpoint
|
||||||
IntrospectionEndpoint() *Endpoint
|
IntrospectionEndpoint() Endpoint
|
||||||
UserinfoEndpoint() *Endpoint
|
UserinfoEndpoint() Endpoint
|
||||||
RevocationEndpoint() *Endpoint
|
RevocationEndpoint() Endpoint
|
||||||
EndSessionEndpoint() *Endpoint
|
EndSessionEndpoint() Endpoint
|
||||||
KeysEndpoint() *Endpoint
|
KeysEndpoint() Endpoint
|
||||||
DeviceAuthorizationEndpoint() *Endpoint
|
DeviceAuthorizationEndpoint() Endpoint
|
||||||
CheckSessionIframe() *Endpoint
|
|
||||||
|
|
||||||
AuthMethodPostSupported() bool
|
AuthMethodPostSupported() bool
|
||||||
CodeMethodS256Supported() bool
|
CodeMethodS256Supported() bool
|
||||||
|
@ -50,53 +47,11 @@ type Configuration interface {
|
||||||
|
|
||||||
SupportedUILocales() []language.Tag
|
SupportedUILocales() []language.Tag
|
||||||
DeviceAuthorization() DeviceAuthorizationConfig
|
DeviceAuthorization() DeviceAuthorizationConfig
|
||||||
|
|
||||||
BackChannelLogoutSupported() bool
|
|
||||||
BackChannelLogoutSessionSupported() bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type IssuerFromRequest func(r *http.Request) string
|
type IssuerFromRequest func(r *http.Request) string
|
||||||
|
|
||||||
func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) {
|
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) {
|
return func(allowInsecure bool) (IssuerFromRequest, error) {
|
||||||
issuerPath, err := url.Parse(path)
|
issuerPath, err := url.Parse(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -106,28 +61,11 @@ func issuerFromForwardedOrHost(path string, c *issuerConfig) func(bool) (IssuerF
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return func(r *http.Request) string {
|
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)
|
return dynamicIssuer(r.Host, path, allowInsecure)
|
||||||
}, nil
|
}, 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) {
|
func StaticIssuer(issuer string) func(bool) (IssuerFromRequest, error) {
|
||||||
return func(allowInsecure bool) (IssuerFromRequest, error) {
|
return func(allowInsecure bool) (IssuerFromRequest, error) {
|
||||||
if err := ValidateIssuer(issuer, allowInsecure); err != nil {
|
if err := ValidateIssuer(issuer, allowInsecure); err != nil {
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateIssuer(t *testing.T) {
|
func TestValidateIssuer(t *testing.T) {
|
||||||
|
@ -236,7 +234,7 @@ func TestIssuerFromHost(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"custom path insecure",
|
"custom path unsecure",
|
||||||
args{
|
args{
|
||||||
path: "/custom/",
|
path: "/custom/",
|
||||||
allowInsecure: true,
|
allowInsecure: true,
|
||||||
|
@ -263,132 +261,6 @@ func TestIssuerFromHost(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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=<identifier>;for=<identifier>;host=<host>;proto=<http|https>
|
|
||||||
{
|
|
||||||
"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) {
|
func TestStaticIssuer(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
issuer string
|
issuer string
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
|
"github.com/zitadel/oidc/v2/pkg/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Crypto interface {
|
type Crypto interface {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue