Compare commits
150 commits
feat_form_
...
main
Author | SHA1 | Date | |
---|---|---|---|
653b807f5d | |||
29d69ca2e0 | |||
53c4d07b45 | |||
154fbe6420 | |||
|
d6e37fa741 | ||
|
8e1e5174fd | ||
|
5618487a88 | ||
|
187878de63 | ||
|
e127c66db2 | ||
|
e1415ef2f3 | ||
|
f94bd541d7 | ||
|
7d57aaa999 | ||
|
668fb0d37a | ||
|
4ed4d257ab | ||
|
4f0ed79c0a | ||
|
5913c5a074 | ||
|
b917cdc2e3 | ||
|
cb3ec3ac5f | ||
|
7cc5fb6568 | ||
|
92972fd30f | ||
|
c51628ea27 | ||
|
7096406e71 | ||
|
c91db9e47b | ||
|
f648c61cab | ||
|
30acdaf63a | ||
|
aeda5d7178 | ||
|
f3ee647005 | ||
|
c401ad6cb8 | ||
|
2c64de821d | ||
|
efd6fdad7a | ||
|
7a767d8568 | ||
|
eb2f912c5e | ||
|
6a80712fbe | ||
|
4ef9529012 | ||
|
eb98343a65 | ||
|
add254f60c | ||
|
b1e5aca629 | ||
|
c03a8c59ca | ||
|
37dd41e49b | ||
|
03e5ff8345 | ||
|
c3c1bd3a40 | ||
|
0d46df908e | ||
|
4250aad1f7 | ||
|
8c9a536058 | ||
|
24c96c361d | ||
|
de2fd41f40 | ||
|
867a4806fd | ||
|
1f6a0d5d89 | ||
|
a0f67c0b4b | ||
|
8d971dcad8 | ||
|
6c90652dfb | ||
|
b36a8e2ec1 | ||
|
9a93b7c70d | ||
|
cf6ce69d79 | ||
|
2513e21531 | ||
|
057601ff3f | ||
|
67bd2f5720 | ||
|
e2de68a7dd | ||
|
a7833f828c | ||
|
6d20928028 | ||
|
1464268851 | ||
|
897c720070 | ||
|
8afb8b8d5f | ||
|
87ab011157 | ||
|
f194951e61 | ||
|
fbf009fe75 | ||
|
f1e4cb2245 | ||
|
24869d2811 | ||
|
9f7cbb0dbf | ||
|
5ae555e191 | ||
|
2abae36bd9 | ||
|
97d7b28fc0 | ||
|
61c3bb887b | ||
|
3b64e792ed | ||
|
b555396744 | ||
|
98c1ab755d | ||
|
6c28e8cb4b | ||
|
e1633bdb93 | ||
|
5e464b4ed8 | ||
|
52e8b651d3 | ||
|
67688db4c1 | ||
|
1e75773eaa | ||
|
99301930ed | ||
|
0aa61b0b98 | ||
|
de034c8d24 | ||
|
b6f3b1e65b | ||
|
6f0a630ad4 | ||
|
8f80225a20 | ||
|
b9bcd6aef9 | ||
|
7b8be4387a | ||
|
e5a428d4be | ||
|
fc6716bf22 | ||
|
d6b4dc6b2f | ||
|
e87f433e09 | ||
|
954802b63b | ||
|
a09d9f7390 | ||
|
371a5aaab4 | ||
|
1c2dc2c0e1 | ||
|
da4e683bd3 | ||
|
a7b5355580 | ||
|
9ecdd0cf9a | ||
|
7a8f8ade4d | ||
|
7037344cf4 | ||
|
7714a3b113 | ||
|
8a47532a8e | ||
|
7437309a42 | ||
|
6d1231cb37 | ||
|
20d0f189a8 | ||
|
30184ae054 | ||
|
5a84d8c4bc | ||
|
24d43f538e | ||
|
37ca0e472a | ||
|
099081fc1e | ||
|
3e329dd049 | ||
|
3512c72f1c | ||
|
79daaf1a7a | ||
|
68d4e08f6d | ||
|
959376bde7 | ||
|
a77d773ca3 | ||
|
3fa4891f3e | ||
|
33f8df7eb2 | ||
|
06f37f84c1 | ||
|
8a21d38136 | ||
|
e75a061807 | ||
|
33485b82ba | ||
|
370738772a | ||
|
a3b73a6950 | ||
|
5cdb65c30b | ||
|
d729c22526 | ||
|
c89d0ed970 | ||
|
910f55ea7b | ||
|
56397f88d5 | ||
|
4d63d68c9e | ||
|
9afc07c0cb | ||
|
aae9c86f1a | ||
|
565a022e91 | ||
|
03f3bc693b | ||
|
0ffd13c780 | ||
|
1b94f796eb | ||
|
ad79802968 | ||
|
1532a5c78b | ||
|
0fe7c3307f | ||
|
7069813ec7 | ||
|
88209ac11d | ||
|
bdcccc3303 | ||
|
d18aba8cb3 | ||
|
e3e48882df | ||
|
5ef597b1db | ||
|
fc743a69c7 | ||
|
7bac3c6f40 |
140 changed files with 2980 additions and 834 deletions
|
@ -2,6 +2,7 @@ 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:
|
|
@ -1,6 +1,7 @@
|
|||
name: 📄 Documentation
|
||||
description: Create an issue for missing or wrong documentation.
|
||||
labels: ["docs"]
|
||||
type: task
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
|
@ -1,11 +1,12 @@
|
|||
name: 🛠️ Improvement
|
||||
description: "Create an new issue for an improvment in ZITADEL"
|
||||
labels: ["improvement"]
|
||||
labels: ["enhancement"]
|
||||
type: enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this improvement request
|
||||
Thanks for taking the time to fill out this proposal / feature reqeust
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: add issue
|
||||
uses: actions/add-to-project@v0.6.0
|
||||
uses: actions/add-to-project@v1.0.2
|
||||
if: ${{ github.event_name == 'issues' }}
|
||||
with:
|
||||
# You can target a repository in a different organization
|
||||
|
@ -28,7 +28,7 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
||||
- name: add pr
|
||||
uses: actions/add-to-project@v0.6.0
|
||||
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
|
|
@ -14,11 +14,11 @@ on:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: ['1.21', '1.22']
|
||||
go: ['1.23', '1.24']
|
||||
name: Go ${{ matrix.go }} test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -27,12 +27,12 @@ jobs:
|
|||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/...
|
||||
- uses: codecov/codecov-action@v4.1.0
|
||||
- uses: codecov/codecov-action@v5.4.3
|
||||
with:
|
||||
file: ./profile.cov
|
||||
name: codecov-go
|
||||
release:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [test]
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }}
|
||||
env:
|
44
.github/ISSUE_TEMPLATE/proposal.yaml
vendored
44
.github/ISSUE_TEMPLATE/proposal.yaml
vendored
|
@ -1,44 +0,0 @@
|
|||
name: 💡 Proposal / Feature request
|
||||
description: "Create an issue for a feature request/proposal."
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this proposal / feature reqeust
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
options:
|
||||
- label:
|
||||
I could not find a solution in the existing issues, docs, nor discussions
|
||||
required: true
|
||||
- label:
|
||||
I have joined the [ZITADEL chat](https://zitadel.com/chat)
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Describe your problem
|
||||
description: Please describe your problem this proposal / feature is supposed to solve.
|
||||
placeholder: Describe the problem you have.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe your ideal solution
|
||||
description: Which solution do you propose?
|
||||
placeholder: As a [type of user], I want [some goal] so that [some reason].
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version of the OIDC Library are you using.
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Please add any other infos that could be useful.
|
108
README.md
108
README.md
|
@ -2,10 +2,10 @@
|
|||
|
||||
[](https://github.com/semantic-release/semantic-release)
|
||||
[](https://github.com/zitadel/oidc/actions)
|
||||
[](https://pkg.go.dev/github.com/zitadel/oidc)
|
||||
[](https://pkg.go.dev/github.com/zitadel/oidc/v3)
|
||||
[](https://github.com/zitadel/oidc/blob/master/LICENSE)
|
||||
[](https://github.com/zitadel/oidc/releases)
|
||||
[](https://goreportcard.com/report/github.com/zitadel/oidc)
|
||||
[](https://goreportcard.com/report/github.com/zitadel/oidc/v3)
|
||||
[](https://codecov.io/gh/zitadel/oidc)
|
||||
|
||||
[](https://openid.net/certification/)
|
||||
|
@ -21,9 +21,10 @@ Whenever possible we tried to reuse / extend existing packages like `OAuth2 for
|
|||
## Basic Overview
|
||||
|
||||
The most important packages of the library:
|
||||
|
||||
<pre>
|
||||
/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)
|
||||
/rs definition and implementation of an OAuth Resource Server (API)
|
||||
/op definition and implementation of an OIDC OpenID Provider (server)
|
||||
|
@ -37,6 +38,10 @@ The most important packages of the library:
|
|||
/server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI
|
||||
</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
|
||||
|
||||
Check the `/example` folder where example code for different scenarios is located.
|
||||
|
@ -50,48 +55,84 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid
|
|||
```
|
||||
|
||||
- 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`
|
||||
- the OP will redirect you to the client app, which displays the user info
|
||||
|
||||
for the dynamic issuer, just start it with:
|
||||
|
||||
```bash
|
||||
go run github.com/zitadel/oidc/v3/example/server/dynamic
|
||||
```
|
||||
```
|
||||
|
||||
the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with:
|
||||
|
||||
```bash
|
||||
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app
|
||||
```
|
||||
|
||||
> Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`)
|
||||
|
||||
### 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
|
||||
|
||||
| | Relying party | OpenID Provider | Specification |
|
||||
| -------------------- | ------------- | --------------- | ----------------------------------------- |
|
||||
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] |
|
||||
| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] |
|
||||
| Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] |
|
||||
| Client Credentials | yes | yes | OpenID Connect Core 1.0, [Section 9][4] |
|
||||
| Refresh Token | yes | yes | OpenID Connect Core 1.0, [Section 12][5] |
|
||||
| Discovery | yes | yes | OpenID Connect [Discovery][6] 1.0 |
|
||||
| JWT Profile | yes | yes | [RFC 7523][7] |
|
||||
| PKCE | yes | yes | [RFC 7636][8] |
|
||||
| Token Exchange | yes | yes | [RFC 8693][9] |
|
||||
| Device Authorization | yes | yes | [RFC 8628][10] |
|
||||
| mTLS | not yet | not yet | [RFC 8705][11] |
|
||||
| | Relying party | OpenID Provider | Specification |
|
||||
| -------------------- | ------------- | --------------- | -------------------------------------------- |
|
||||
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] |
|
||||
| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] |
|
||||
| Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] |
|
||||
| Client Credentials | yes | yes | OpenID Connect Core 1.0, [Section 9][4] |
|
||||
| Refresh Token | yes | yes | OpenID Connect Core 1.0, [Section 12][5] |
|
||||
| Discovery | yes | yes | OpenID Connect [Discovery][6] 1.0 |
|
||||
| JWT Profile | yes | yes | [RFC 7523][7] |
|
||||
| PKCE | yes | yes | [RFC 7636][8] |
|
||||
| Token Exchange | yes | yes | [RFC 8693][9] |
|
||||
| Device Authorization | yes | yes | [RFC 8628][10] |
|
||||
| mTLS | not yet | not yet | [RFC 8705][11] |
|
||||
| Back-Channel Logout | not yet | yes | OpenID Connect [Back-Channel Logout][12] 1.0 |
|
||||
|
||||
[1]: <https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth> "3.1. Authentication using the Authorization Code Flow"
|
||||
[2]: <https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth> "3.2. Authentication using the Implicit Flow"
|
||||
[3]: <https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth> "3.3. Authentication using the Hybrid Flow"
|
||||
[4]: <https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication> "9. Client Authentication"
|
||||
[5]: <https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens> "12. Using Refresh Tokens"
|
||||
[6]: <https://openid.net/specs/openid-connect-discovery-1_0.html> "OpenID Connect Discovery 1.0 incorporating errata set 1"
|
||||
[7]: <https://www.rfc-editor.org/rfc/rfc7523.html> "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
|
||||
[8]: <https://www.rfc-editor.org/rfc/rfc7636.html> "Proof Key for Code Exchange by OAuth Public Clients"
|
||||
[9]: <https://www.rfc-editor.org/rfc/rfc8693.html> "OAuth 2.0 Token Exchange"
|
||||
[10]: <https://www.rfc-editor.org/rfc/rfc8628.html> "OAuth 2.0 Device Authorization Grant"
|
||||
[11]: <https://www.rfc-editor.org/rfc/rfc8705.html> "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
|
||||
[1]: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth "3.1. Authentication using the Authorization Code Flow"
|
||||
[2]: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth "3.2. Authentication using the Implicit Flow"
|
||||
[3]: https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth "3.3. Authentication using the Hybrid Flow"
|
||||
[4]: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication "9. Client Authentication"
|
||||
[5]: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens "12. Using Refresh Tokens"
|
||||
[6]: https://openid.net/specs/openid-connect-discovery-1_0.html "OpenID Connect Discovery 1.0 incorporating errata set 1"
|
||||
[7]: https://www.rfc-editor.org/rfc/rfc7523.html "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
|
||||
[8]: https://www.rfc-editor.org/rfc/rfc7636.html "Proof Key for Code Exchange by OAuth Public Clients"
|
||||
[9]: https://www.rfc-editor.org/rfc/rfc8693.html "OAuth 2.0 Token Exchange"
|
||||
[10]: https://www.rfc-editor.org/rfc/rfc8628.html "OAuth 2.0 Device Authorization Grant"
|
||||
[11]: https://www.rfc-editor.org/rfc/rfc8705.html "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
|
||||
[12]: https://openid.net/specs/openid-connect-backchannel-1_0.html "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1"
|
||||
|
||||
## Contributors
|
||||
|
||||
|
@ -110,14 +151,14 @@ For your convenience you can find the relevant guides linked below.
|
|||
|
||||
## 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:.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| <1.21 | :x: |
|
||||
| 1.21 | :white_check_mark: |
|
||||
| 1.22 | :white_check_mark: |
|
||||
| <1.23 | :x: |
|
||||
| 1.23 | :white_check_mark: |
|
||||
| 1.24 | :white_check_mark: |
|
||||
|
||||
## Why another library
|
||||
|
||||
|
@ -148,5 +189,4 @@ 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
|
||||
language governing permissions and limitations under the License.
|
||||
|
||||
|
||||
[^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892
|
||||
|
|
370
UPGRADING.md
Normal file
370
UPGRADING.md
Normal file
|
@ -0,0 +1,370 @@
|
|||
# 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
|
||||
```
|
|
@ -13,8 +13,8 @@ import (
|
|||
"github.com/go-chi/chi/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rs"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -14,10 +14,10 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -32,6 +32,7 @@ func main() {
|
|||
issuer := os.Getenv("ISSUER")
|
||||
port := os.Getenv("PORT")
|
||||
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
||||
responseMode := os.Getenv("RESPONSE_MODE")
|
||||
|
||||
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
|
||||
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
||||
|
@ -55,6 +56,7 @@ func main() {
|
|||
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
|
||||
rp.WithHTTPClient(client),
|
||||
rp.WithLogger(logger),
|
||||
rp.WithSigningAlgsFromDiscovery(),
|
||||
}
|
||||
if clientSecret == "" {
|
||||
options = append(options, rp.WithPKCE(cookieHandler))
|
||||
|
@ -77,20 +79,37 @@ func main() {
|
|||
return uuid.New().String()
|
||||
}
|
||||
|
||||
urlOptions := []rp.URLParamOpt{
|
||||
rp.WithPromptURLParam("Welcome back!"),
|
||||
}
|
||||
|
||||
if responseMode != "" {
|
||||
urlOptions = append(urlOptions, rp.WithResponseModeURLParam(oidc.ResponseMode(responseMode)))
|
||||
}
|
||||
|
||||
// register the AuthURLHandler at your preferred path.
|
||||
// the AuthURLHandler creates the auth request and redirects the user to the auth server.
|
||||
// including state handling with secure cookie and the possibility to use PKCE.
|
||||
// Prompts can optionally be set to inform the server of
|
||||
// any messages that need to be prompted back to the user.
|
||||
http.Handle("/login", rp.AuthURLHandler(state, provider, rp.WithPromptURLParam("Welcome back!")))
|
||||
http.Handle("/login", rp.AuthURLHandler(
|
||||
state,
|
||||
provider,
|
||||
urlOptions...,
|
||||
))
|
||||
|
||||
// for demonstration purposes the returned userinfo response is written as JSON object onto response
|
||||
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
||||
fmt.Println("access token", tokens.AccessToken)
|
||||
fmt.Println("refresh token", tokens.RefreshToken)
|
||||
fmt.Println("id token", tokens.IDToken)
|
||||
|
||||
data, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("content-type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
|
|
|
@ -45,8 +45,8 @@ import (
|
|||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -10,10 +10,10 @@ import (
|
|||
"golang.org/x/oauth2"
|
||||
githubOAuth "golang.org/x/oauth2/github"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp/cli"
|
||||
"github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp/cli"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client/profile"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/profile"
|
||||
)
|
||||
|
||||
var client = http.DefaultClient
|
||||
|
|
40
example/server/config/config.go
Normal file
40
example/server/config/config.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
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
|
||||
}
|
77
example/server/config/config_test.go
Normal file
77
example/server/config/config_test.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
"github.com/go-chi/chi/v5"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/oidc/v3/example/server/storage"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -8,10 +8,10 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
type deviceAuthenticate interface {
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
type login struct {
|
||||
|
|
|
@ -12,22 +12,13 @@ import (
|
|||
"github.com/zitadel/logging"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/oidc/v3/example/server/storage"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
const (
|
||||
pathLoggedOut = "/logged-out"
|
||||
)
|
||||
|
||||
func init() {
|
||||
storage.RegisterClients(
|
||||
storage.NativeClient("native"),
|
||||
storage.WebClient("web", "secret"),
|
||||
storage.WebClient("api", "secret"),
|
||||
)
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
op.Storage
|
||||
authenticate
|
||||
|
@ -56,7 +47,7 @@ func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer
|
|||
// for simplicity, we provide a very small default page for users who have signed out
|
||||
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Write([]byte("signed out successfully"))
|
||||
// no need to check/log error, this will be handeled by the middleware.
|
||||
// no need to check/log error, this will be handled by the middleware.
|
||||
})
|
||||
|
||||
// creation of the OpenIDProvider with the just created in-memory Storage
|
||||
|
@ -80,7 +71,7 @@ func SetupServer(issuer string, storage Storage, logger *slog.Logger, wrapServer
|
|||
|
||||
handler := http.Handler(provider)
|
||||
if wrapServer {
|
||||
handler = op.RegisterLegacyServer(op.NewLegacyServer(provider, *op.DefaultEndpoints))
|
||||
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)
|
||||
|
|
|
@ -6,36 +6,53 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/zitadel/oidc/v3/example/server/exampleop"
|
||||
"github.com/zitadel/oidc/v3/example/server/storage"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/config"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/exampleop"
|
||||
"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() {
|
||||
//we will run on :9998
|
||||
port := "9998"
|
||||
//which gives us the issuer: http://localhost:9998/
|
||||
issuer := fmt.Sprintf("http://localhost:%s/", port)
|
||||
|
||||
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
||||
// this might be the layer for accessing your database
|
||||
// in this example it will be handled in-memory
|
||||
storage := storage.NewStorage(storage.NewUserStore(issuer))
|
||||
|
||||
cfg := config.FromEnvVars(&config.Config{Port: "9998"})
|
||||
logger := slog.New(
|
||||
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: slog.LevelDebug,
|
||||
}),
|
||||
)
|
||||
|
||||
//which gives us the issuer: http://localhost:9998/
|
||||
issuer := fmt.Sprintf("http://localhost:%s/", cfg.Port)
|
||||
|
||||
storage.RegisterClients(
|
||||
storage.NativeClient("native", cfg.RedirectURI...),
|
||||
storage.WebClient("web", "secret", cfg.RedirectURI...),
|
||||
storage.WebClient("api", "secret", cfg.RedirectURI...),
|
||||
)
|
||||
|
||||
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
||||
// this might be the layer for accessing your database
|
||||
// in this example it will be handled in-memory
|
||||
store, err := getUserStore(cfg)
|
||||
if err != nil {
|
||||
logger.Error("cannot create UserStore", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
storage := storage.NewStorage(store)
|
||||
router := exampleop.SetupServer(issuer, storage, logger, false)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Addr: ":" + cfg.Port,
|
||||
Handler: router,
|
||||
}
|
||||
logger.Info("server listening, press ctrl+c to stop", "addr", fmt.Sprintf("http://localhost:%s/", port))
|
||||
err := server.ListenAndServe()
|
||||
if err != http.ErrServerClosed {
|
||||
logger.Info("server listening, press ctrl+c to stop", "addr", issuer)
|
||||
if server.ListenAndServe() != http.ErrServerClosed {
|
||||
logger.Error("server terminated", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ package storage
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -184,10 +184,10 @@ func WebClient(id, secret string, redirectURIs ...string) *Client {
|
|||
applicationType: op.ApplicationTypeWeb,
|
||||
authMethod: oidc.AuthMethodBasic,
|
||||
loginURL: defaultLoginURL,
|
||||
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
|
||||
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode, oidc.ResponseTypeIDTokenOnly, oidc.ResponseTypeIDToken},
|
||||
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken, oidc.GrantTypeTokenExchange},
|
||||
accessTokenType: op.AccessTokenTypeBearer,
|
||||
devMode: false,
|
||||
devMode: true,
|
||||
idTokenUserinfoClaimsAssertion: false,
|
||||
clockSkew: 0,
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -35,6 +35,7 @@ type AuthRequest struct {
|
|||
UserID string
|
||||
Scopes []string
|
||||
ResponseType oidc.ResponseType
|
||||
ResponseMode oidc.ResponseMode
|
||||
Nonce string
|
||||
CodeChallenge *OIDCCodeChallenge
|
||||
|
||||
|
@ -100,7 +101,7 @@ func (a *AuthRequest) GetResponseType() oidc.ResponseType {
|
|||
}
|
||||
|
||||
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
|
||||
return "" // we won't handle response mode in this example
|
||||
return a.ResponseMode
|
||||
}
|
||||
|
||||
func (a *AuthRequest) GetScopes() []string {
|
||||
|
@ -120,7 +121,7 @@ func (a *AuthRequest) Done() bool {
|
|||
}
|
||||
|
||||
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
|
||||
prompts := make([]string, len(oidcPrompt))
|
||||
prompts := make([]string, 0, len(oidcPrompt))
|
||||
for _, oidcPrompt := range oidcPrompt {
|
||||
switch oidcPrompt {
|
||||
case oidc.PromptNone,
|
||||
|
@ -154,6 +155,7 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques
|
|||
UserID: userID,
|
||||
Scopes: authReq.Scopes,
|
||||
ResponseType: authReq.ResponseType,
|
||||
ResponseMode: authReq.ResponseMode,
|
||||
Nonce: authReq.Nonce,
|
||||
CodeChallenge: &OIDCCodeChallenge{
|
||||
Challenge: authReq.CodeChallenge,
|
||||
|
@ -162,6 +164,15 @@ 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 {
|
||||
Challenge string
|
||||
Method string
|
||||
|
|
|
@ -11,11 +11,11 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
|
||||
|
@ -151,6 +151,9 @@ 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
|
||||
// therefore we will also just check this boolean if the request / login has been finished
|
||||
request.done = true
|
||||
|
||||
request.authTime = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("username or password wrong")
|
||||
|
@ -295,15 +298,19 @@ 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
|
||||
// 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 {
|
||||
return "", "", time.Time{}, err
|
||||
}
|
||||
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
|
||||
if err != nil {
|
||||
|
||||
if err := s.renewRefreshToken(currentRefreshToken, newRefreshToken, accessToken.ID); err != nil {
|
||||
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) {
|
||||
|
@ -385,14 +392,9 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID
|
|||
if refreshToken.ApplicationID != clientID {
|
||||
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
|
||||
}
|
||||
// if it is a refresh token, you will have to remove the access token as well
|
||||
delete(s.refreshTokens, refreshToken.ID)
|
||||
for _, accessToken := range s.tokens {
|
||||
if accessToken.RefreshTokenID == refreshToken.ID {
|
||||
delete(s.tokens, accessToken.ID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// if it is a refresh token, you will have to remove the access token as well
|
||||
delete(s.tokens, refreshToken.AccessToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -488,6 +490,9 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserI
|
|||
// 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)
|
||||
}
|
||||
|
||||
|
@ -594,33 +599,41 @@ func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime
|
|||
Audience: accessToken.Audience,
|
||||
Expiration: time.Now().Add(5 * time.Hour),
|
||||
Scopes: accessToken.Scopes,
|
||||
AccessToken: accessToken.ID,
|
||||
}
|
||||
s.refreshTokens[token.ID] = token
|
||||
return token.Token, nil
|
||||
}
|
||||
|
||||
// renewRefreshToken checks the provided refresh_token and creates a new one based on the current
|
||||
func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) {
|
||||
//
|
||||
// [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()
|
||||
defer s.lock.Unlock()
|
||||
refreshToken, ok := s.refreshTokens[currentRefreshToken]
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("invalid refresh token")
|
||||
return fmt.Errorf("invalid refresh token")
|
||||
}
|
||||
// deletes the refresh token and all access tokens which were issued based on this refresh token
|
||||
// deletes the refresh token
|
||||
delete(s.refreshTokens, currentRefreshToken)
|
||||
for _, token := range s.tokens {
|
||||
if token.RefreshTokenID == currentRefreshToken {
|
||||
delete(s.tokens, token.ID)
|
||||
break
|
||||
}
|
||||
|
||||
// delete the access token which was issued based on this refresh token
|
||||
delete(s.tokens, refreshToken.AccessToken)
|
||||
|
||||
if refreshToken.Expiration.Before(time.Now()) {
|
||||
return fmt.Errorf("expired refresh token")
|
||||
}
|
||||
|
||||
// creates a new refresh token based on the current one
|
||||
token := uuid.NewString()
|
||||
refreshToken.Token = token
|
||||
refreshToken.ID = token
|
||||
s.refreshTokens[token] = refreshToken
|
||||
return token, refreshToken.ID, nil
|
||||
refreshToken.Token = newRefreshToken
|
||||
refreshToken.ID = newRefreshToken
|
||||
refreshToken.Expiration = time.Now().Add(5 * time.Hour)
|
||||
refreshToken.AccessToken = newAccessToken
|
||||
s.refreshTokens[newRefreshToken] = refreshToken
|
||||
return nil
|
||||
}
|
||||
|
||||
// accessToken will store an access_token in-memory based on the provided information
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
type multiStorage struct {
|
||||
|
|
|
@ -22,4 +22,5 @@ type RefreshToken struct {
|
|||
ApplicationID string
|
||||
Expiration time.Time
|
||||
Scopes []string
|
||||
AccessToken string // Token.ID
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package storage
|
|||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
@ -35,6 +37,18 @@ type userStore struct {
|
|||
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 {
|
||||
hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
|
||||
return userStore{
|
||||
|
|
70
example/server/storage/user_test.go
Normal file
70
example/server/storage/user_test.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
43
go.mod
43
go.mod
|
@ -1,41 +1,40 @@
|
|||
module github.com/zitadel/oidc/v3
|
||||
module git.christmann.info/LARA/zitadel-oidc/v3
|
||||
|
||||
go 1.21
|
||||
go 1.23.7
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-jose/go-jose/v3 v3.0.2
|
||||
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/google/go-github/v31 v31.0.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/jeremija/gosubmit v0.2.7
|
||||
github.com/jeremija/gosubmit v0.2.8
|
||||
github.com/muhlemmer/gu v0.3.1
|
||||
github.com/muhlemmer/httpforwarded v0.1.0
|
||||
github.com/rs/cors v1.10.1
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/zitadel/logging v0.6.0
|
||||
github.com/zitadel/schema v1.3.0
|
||||
go.opentelemetry.io/otel v1.24.0
|
||||
go.opentelemetry.io/otel/trace v1.24.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/text v0.14.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/zitadel/logging v0.6.2
|
||||
github.com/zitadel/schema v1.3.1
|
||||
go.opentelemetry.io/otel v1.29.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/text v0.26.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
110
go.sum
110
go.sum
|
@ -1,27 +1,21 @@
|
|||
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-jose/go-jose/v3 v3.0.2 h1:2Edjn8Nrb44UvTdp84KU0bBPs1cO7noRCybtS3eJEUQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.2/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
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.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.3.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.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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
|
||||
|
@ -35,8 +29,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
|
||||
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
|
||||
github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA=
|
||||
github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
|
@ -47,100 +41,68 @@ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/
|
|||
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
|
||||
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank=
|
||||
github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
|
||||
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
|
||||
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
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-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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
tu "github.com/zitadel/oidc/v3/internal/testutil"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
var custom = map[string]any{
|
||||
|
|
|
@ -8,9 +8,9 @@ import (
|
|||
"errors"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
// KeySet implements oidc.Keys
|
||||
|
|
|
@ -2,7 +2,6 @@ package client
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -11,20 +10,27 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/zitadel/logging"
|
||||
"go.opentelemetry.io/otel"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
var Encoder = httphelper.Encoder(oidc.NewEncoder())
|
||||
var (
|
||||
Encoder = httphelper.Encoder(oidc.NewEncoder())
|
||||
Tracer = otel.Tracer("github.com/zitadel/oidc/pkg/client")
|
||||
)
|
||||
|
||||
// Discover calls the discovery endpoint of the provided issuer and returns its configuration
|
||||
// It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url
|
||||
func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) {
|
||||
ctx, span := Tracer.Start(ctx, "Discover")
|
||||
defer span.End()
|
||||
|
||||
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
|
||||
if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" {
|
||||
wellKnown = wellKnownUrl[0]
|
||||
|
@ -36,7 +42,7 @@ func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellK
|
|||
discoveryConfig := new(oidc.DiscoveryConfiguration)
|
||||
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Join(oidc.ErrDiscoveryFailed, err)
|
||||
}
|
||||
if logger, ok := logging.FromContext(ctx); ok {
|
||||
logger.Debug("discover", "config", discoveryConfig)
|
||||
|
@ -58,6 +64,9 @@ func CallTokenEndpoint(ctx context.Context, request any, caller TokenEndpointCal
|
|||
}
|
||||
|
||||
func callTokenEndpoint(ctx context.Context, request any, authFn any, caller TokenEndpointCaller) (newToken *oauth2.Token, err error) {
|
||||
ctx, span := Tracer.Start(ctx, "callTokenEndpoint")
|
||||
defer span.End()
|
||||
|
||||
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -86,7 +95,15 @@ type EndSessionCaller interface {
|
|||
}
|
||||
|
||||
func CallEndSessionEndpoint(ctx context.Context, request any, authFn any, caller EndSessionCaller) (*url.URL, error) {
|
||||
req, err := httphelper.FormRequest(ctx, caller.GetEndSessionEndpoint(), request, Encoder, authFn)
|
||||
ctx, span := Tracer.Start(ctx, "CallEndSessionEndpoint")
|
||||
defer span.End()
|
||||
|
||||
endpoint := caller.GetEndSessionEndpoint()
|
||||
if endpoint == "" {
|
||||
return nil, fmt.Errorf("end session %w", ErrEndpointNotSet)
|
||||
}
|
||||
|
||||
req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -129,7 +146,15 @@ type RevokeRequest struct {
|
|||
}
|
||||
|
||||
func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller RevokeCaller) error {
|
||||
req, err := httphelper.FormRequest(ctx, caller.GetRevokeEndpoint(), request, Encoder, authFn)
|
||||
ctx, span := Tracer.Start(ctx, "CallRevokeEndpoint")
|
||||
defer span.End()
|
||||
|
||||
endpoint := caller.GetRevokeEndpoint()
|
||||
if endpoint == "" {
|
||||
return fmt.Errorf("revoke %w", ErrEndpointNotSet)
|
||||
}
|
||||
|
||||
req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -157,6 +182,9 @@ func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller Rev
|
|||
}
|
||||
|
||||
func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) {
|
||||
ctx, span := Tracer.Start(ctx, "CallTokenExchangeEndpoint")
|
||||
defer span.End()
|
||||
|
||||
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -169,12 +197,12 @@ func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, cal
|
|||
}
|
||||
|
||||
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
|
||||
privateKey, err := crypto.BytesToPrivateKey(key)
|
||||
privateKey, algorithm, err := crypto.BytesToPrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signingKey := jose.SigningKey{
|
||||
Algorithm: jose.RS256,
|
||||
Algorithm: algorithm,
|
||||
Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID},
|
||||
}
|
||||
return jose.NewSigner(signingKey, &jose.SignerOptions{})
|
||||
|
@ -198,7 +226,15 @@ type DeviceAuthorizationCaller interface {
|
|||
}
|
||||
|
||||
func CallDeviceAuthorizationEndpoint(ctx context.Context, request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller, authFn any) (*oidc.DeviceAuthorizationResponse, error) {
|
||||
req, err := httphelper.FormRequest(ctx, caller.GetDeviceAuthorizationEndpoint(), request, Encoder, authFn)
|
||||
ctx, span := Tracer.Start(ctx, "CallDeviceAuthorizationEndpoint")
|
||||
defer span.End()
|
||||
|
||||
endpoint := caller.GetDeviceAuthorizationEndpoint()
|
||||
if endpoint == "" {
|
||||
return nil, fmt.Errorf("device authorization %w", ErrEndpointNotSet)
|
||||
}
|
||||
|
||||
req, err := httphelper.FormRequest(ctx, endpoint, request, Encoder, authFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -219,6 +255,9 @@ type DeviceAccessTokenRequest struct {
|
|||
}
|
||||
|
||||
func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
|
||||
ctx, span := Tracer.Start(ctx, "CallDeviceAccessTokenEndpoint")
|
||||
defer span.End()
|
||||
|
||||
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -227,28 +266,17 @@ func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTok
|
|||
req.SetBasicAuth(request.ClientID, request.ClientSecret)
|
||||
}
|
||||
|
||||
httpResp, err := caller.HttpClient().Do(req)
|
||||
if err != nil {
|
||||
resp := new(oidc.AccessTokenResponse)
|
||||
if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
|
||||
ctx, span := Tracer.Start(ctx, "PollDeviceAccessTokenEndpoint")
|
||||
defer span.End()
|
||||
|
||||
for {
|
||||
timer := time.After(interval)
|
||||
select {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -22,7 +23,7 @@ func TestDiscover(t *testing.T) {
|
|||
name string
|
||||
args args
|
||||
wantFields *wantFields
|
||||
wantErr bool
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "spotify", // https://github.com/zitadel/oidc/issues/406
|
||||
|
@ -32,17 +33,20 @@ func TestDiscover(t *testing.T) {
|
|||
wantFields: &wantFields{
|
||||
UILocalesSupported: true,
|
||||
},
|
||||
wantErr: false,
|
||||
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...)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.wantFields == nil {
|
||||
return
|
||||
}
|
||||
|
|
5
pkg/client/errors.go
Normal file
5
pkg/client/errors.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package client
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrEndpointNotSet = errors.New("endpoint not set")
|
|
@ -23,14 +23,14 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/zitadel/oidc/v3/example/server/exampleop"
|
||||
"github.com/zitadel/oidc/v3/example/server/storage"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rs"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/tokenexchange"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/exampleop"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/tokenexchange"
|
||||
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"
|
||||
)
|
||||
|
||||
var Logger = slog.New(
|
||||
|
@ -111,6 +111,92 @@ func testRelyingPartySession(t *testing.T, wrapServer bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRelyingPartyWithSigningAlgsFromDiscovery(t *testing.T) {
|
||||
targetURL := "http://local-site"
|
||||
localURL, err := url.Parse(targetURL + "/login?requestID=1234")
|
||||
require.NoError(t, err, "local url")
|
||||
|
||||
t.Log("------- start example OP ------")
|
||||
seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
|
||||
clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
|
||||
clientSecret := "secret"
|
||||
client := storage.WebClient(clientID, clientSecret, targetURL)
|
||||
storage.RegisterClients(client)
|
||||
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
|
||||
var dh deferredHandler
|
||||
opServer := httptest.NewServer(&dh)
|
||||
defer opServer.Close()
|
||||
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, true)
|
||||
|
||||
t.Log("------- create RP ------")
|
||||
provider, err := rp.NewRelyingPartyOIDC(
|
||||
CTX,
|
||||
opServer.URL,
|
||||
clientID,
|
||||
clientSecret,
|
||||
targetURL,
|
||||
[]string{"openid"},
|
||||
rp.WithSigningAlgsFromDiscovery(),
|
||||
)
|
||||
require.NoError(t, err, "new rp")
|
||||
|
||||
t.Log("------- run authorization code flow ------")
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.NoError(t, err, "create cookie jar")
|
||||
httpClient := &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Jar: jar,
|
||||
}
|
||||
state := "state-" + strconv.FormatInt(seed.Int63(), 25)
|
||||
capturedW := httptest.NewRecorder()
|
||||
get := httptest.NewRequest("GET", localURL.String(), nil)
|
||||
rp.AuthURLHandler(func() string { return state }, provider,
|
||||
rp.WithPromptURLParam("Hello, World!", "Goodbye, World!"),
|
||||
rp.WithURLParam("custom", "param"),
|
||||
)(capturedW, get)
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
t.Log("response body (redirect from RP to OP)", capturedW.Body.String())
|
||||
}
|
||||
}()
|
||||
resp := capturedW.Result()
|
||||
startAuthURL, err := resp.Location()
|
||||
require.NoError(t, err, "get redirect")
|
||||
loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL)
|
||||
form := getForm(t, "get login form", httpClient, loginPageURL)
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
t.Logf("login form (unfilled): %s", string(form))
|
||||
}
|
||||
}()
|
||||
postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL,
|
||||
gosubmit.Set("username", "test-user@local-site"),
|
||||
gosubmit.Set("password", "verysecure"),
|
||||
)
|
||||
codeBearingURL := getRedirect(t, "get redirect with code", httpClient, postLoginRedirectURL)
|
||||
capturedW = httptest.NewRecorder()
|
||||
get = httptest.NewRequest("GET", codeBearingURL.String(), nil)
|
||||
var idToken string
|
||||
redirect := func(w http.ResponseWriter, r *http.Request, newTokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
||||
idToken = newTokens.IDToken
|
||||
http.Redirect(w, r, targetURL, http.StatusFound)
|
||||
}
|
||||
rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get)
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
t.Log("token exchange response body", capturedW.Body.String())
|
||||
require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code")
|
||||
}
|
||||
}()
|
||||
|
||||
t.Log("------- verify id token ------")
|
||||
_, err = rp.VerifyIDToken[*oidc.IDTokenClaims](CTX, idToken, provider.IDTokenVerifier())
|
||||
require.NoError(t, err, "verify id token")
|
||||
}
|
||||
|
||||
func TestResourceServerTokenExchange(t *testing.T) {
|
||||
for _, wrapServer := range []bool{false, true} {
|
||||
t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) {
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
// JWTProfileExchange handles the oauth2 jwt profile exchange
|
||||
|
|
|
@ -2,7 +2,7 @@ package client
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -24,7 +24,7 @@ type KeyFile struct {
|
|||
}
|
||||
|
||||
func ConfigFromKeyFile(path string) (*KeyFile, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
type TokenSource interface {
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package rp
|
||||
|
||||
import (
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc/grants/tokenexchange"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc/grants/tokenexchange"
|
||||
)
|
||||
|
||||
// DelegationTokenRequest is an implementation of TokenExchangeRequest
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) {
|
||||
|
@ -33,6 +33,9 @@ func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.
|
|||
// in RFC 8628, section 3.1 and 3.2:
|
||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1
|
||||
func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, authFn any) (*oidc.DeviceAuthorizationResponse, error) {
|
||||
ctx, span := client.Tracer.Start(ctx, "DeviceAuthorization")
|
||||
defer span.End()
|
||||
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAuthorization")
|
||||
req, err := newDeviceClientCredentialsRequest(scopes, rp)
|
||||
if err != nil {
|
||||
|
@ -46,6 +49,9 @@ func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty,
|
|||
// by means of polling as defined in RFC, section 3.3 and 3.4:
|
||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4
|
||||
func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) {
|
||||
ctx, span := client.Tracer.Start(ctx, "DeviceAccessToken")
|
||||
defer span.End()
|
||||
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAccessToken")
|
||||
req := &client.DeviceAccessTokenRequest{
|
||||
DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{
|
||||
|
|
|
@ -7,10 +7,11 @@ import (
|
|||
"net/http"
|
||||
"sync"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet {
|
||||
|
@ -83,6 +84,9 @@ func (i *inflight) result() ([]jose.JSONWebKey, 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)
|
||||
if alg == "" {
|
||||
alg = r.defaultAlg
|
||||
|
@ -135,6 +139,9 @@ func (r *remoteKeySet) exactMatch(jwkID, jwsID string) bool {
|
|||
}
|
||||
|
||||
func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) {
|
||||
ctx, span := client.Tracer.Start(ctx, "verifySignatureRemote")
|
||||
defer span.End()
|
||||
|
||||
keys, err := r.keysFromRemote(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch key for signature validation: %w", err)
|
||||
|
@ -159,6 +166,9 @@ func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey) {
|
|||
// keysFromRemote syncs the key set from the remote set, records the values in the
|
||||
// cache, and returns the key set.
|
||||
func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) {
|
||||
ctx, span := client.Tracer.Start(ctx, "keysFromRemote")
|
||||
defer span.End()
|
||||
|
||||
// Need to lock to inspect the inflight request field.
|
||||
r.mu.Lock()
|
||||
// If there's not a current inflight request, create one.
|
||||
|
@ -182,6 +192,9 @@ func (r *remoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e
|
|||
}
|
||||
|
||||
func (r *remoteKeySet) updateKeys(ctx context.Context) {
|
||||
ctx, span := client.Tracer.Start(ctx, "updateKeys")
|
||||
defer span.End()
|
||||
|
||||
// Sync keys and finish inflight when that's done.
|
||||
keys, err := r.fetchRemoteKeys(ctx)
|
||||
|
||||
|
@ -201,7 +214,10 @@ func (r *remoteKeySet) updateKeys(ctx context.Context) {
|
|||
}
|
||||
|
||||
func (r *remoteKeySet) fetchRemoteKeys(ctx context.Context) ([]jose.JSONWebKey, error) {
|
||||
req, err := http.NewRequest("GET", r.jwksURL, nil)
|
||||
ctx, span := client.Tracer.Start(ctx, "fetchRemoteKeys")
|
||||
defer span.End()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", r.jwksURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: can't create request: %v", err)
|
||||
}
|
||||
|
|
|
@ -9,15 +9,15 @@ import (
|
|||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/zitadel/logging"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -60,7 +60,7 @@ type RelyingParty interface {
|
|||
// UserinfoEndpoint returns the userinfo
|
||||
UserinfoEndpoint() string
|
||||
|
||||
// GetDeviceAuthorizationEndpoint returns the enpoint which can
|
||||
// GetDeviceAuthorizationEndpoint returns the endpoint which can
|
||||
// be used to start a DeviceAuthorization flow.
|
||||
GetDeviceAuthorizationEndpoint() string
|
||||
|
||||
|
@ -90,12 +90,13 @@ var DefaultUnauthorizedHandler UnauthorizedHandler = func(w http.ResponseWriter,
|
|||
}
|
||||
|
||||
type relyingParty struct {
|
||||
issuer string
|
||||
DiscoveryEndpoint string
|
||||
endpoints Endpoints
|
||||
oauthConfig *oauth2.Config
|
||||
oauth2Only bool
|
||||
pkce bool
|
||||
issuer string
|
||||
DiscoveryEndpoint string
|
||||
endpoints Endpoints
|
||||
oauthConfig *oauth2.Config
|
||||
oauth2Only bool
|
||||
pkce bool
|
||||
useSigningAlgsFromDiscovery bool
|
||||
|
||||
httpClient *http.Client
|
||||
cookieHandler *httphelper.CookieHandler
|
||||
|
@ -238,6 +239,9 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rp.useSigningAlgsFromDiscovery {
|
||||
rp.verifierOpts = append(rp.verifierOpts, WithSupportedSigningAlgorithms(discoveryConfiguration.IDTokenSigningAlgValuesSupported...))
|
||||
}
|
||||
endpoints := GetEndpoints(discoveryConfiguration)
|
||||
rp.oauthConfig.Endpoint = endpoints.Endpoint
|
||||
rp.endpoints = endpoints
|
||||
|
@ -348,6 +352,15 @@ func WithLogger(logger *slog.Logger) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithSigningAlgsFromDiscovery appends the [WithSupportedSigningAlgorithms] option to the Verifier Options.
|
||||
// The algorithms returned in the `id_token_signing_alg_values_supported` from the discovery response will be set.
|
||||
func WithSigningAlgsFromDiscovery() Option {
|
||||
return func(rp *relyingParty) error {
|
||||
rp.useSigningAlgsFromDiscovery = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type SignerFromKey func() (jose.Signer, error)
|
||||
|
||||
func SignerFromKeyPath(path string) SignerFromKey {
|
||||
|
@ -388,7 +401,7 @@ func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string {
|
|||
|
||||
// AuthURLHandler extends the `AuthURL` method with a http redirect handler
|
||||
// including handling setting cookie for secure `state` transfer.
|
||||
// Custom paramaters can optionally be set to the redirect URL.
|
||||
// Custom parameters can optionally be set to the redirect URL.
|
||||
func AuthURLHandler(stateFn func() string, rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
opts := make([]AuthURLOpt, len(urlParam))
|
||||
|
@ -428,6 +441,9 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri
|
|||
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
|
||||
}
|
||||
|
@ -445,6 +461,9 @@ func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Tok
|
|||
// CodeExchange handles the oauth2 code exchange, extracting and validating the id_token
|
||||
// returning it parsed together with the oauth2 tokens (access, refresh)
|
||||
func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) {
|
||||
ctx, codeExchangeSpan := client.Tracer.Start(ctx, "CodeExchange")
|
||||
defer codeExchangeSpan.End()
|
||||
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "CodeExchange")
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
|
||||
codeOpts := make([]oauth2.AuthCodeOption, 0)
|
||||
|
@ -452,10 +471,12 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP
|
|||
codeOpts = append(codeOpts, opt()...)
|
||||
}
|
||||
|
||||
ctx, oauthExchangeSpan := client.Tracer.Start(ctx, "OAuthExchange")
|
||||
token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oauthExchangeSpan.End()
|
||||
return verifyTokenResponse[C](ctx, token, rp)
|
||||
}
|
||||
|
||||
|
@ -469,6 +490,9 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP
|
|||
// [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,
|
||||
|
@ -489,14 +513,17 @@ type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.R
|
|||
// Custom parameters can optionally be set to the token URL.
|
||||
func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := client.Tracer.Start(r.Context(), "CodeExchangeHandler")
|
||||
r = r.WithContext(ctx)
|
||||
defer span.End()
|
||||
|
||||
state, err := tryReadStateCookie(w, r, rp)
|
||||
if err != nil {
|
||||
unauthorizedError(w, r, "failed to get state: "+err.Error(), state, rp)
|
||||
return
|
||||
}
|
||||
params := r.URL.Query()
|
||||
if params.Get("error") != "" {
|
||||
rp.ErrorHandler()(w, r, params.Get("error"), params.Get("error_description"), state)
|
||||
if errValue := r.FormValue("error"); errValue != "" {
|
||||
rp.ErrorHandler()(w, r, errValue, r.FormValue("error_description"), state)
|
||||
return
|
||||
}
|
||||
codeOpts := make([]CodeExchangeOpt, len(urlParam))
|
||||
|
@ -514,14 +541,14 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R
|
|||
rp.CookieHandler().DeleteCookie(w, pkceCode)
|
||||
}
|
||||
if rp.Signer() != nil {
|
||||
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer())
|
||||
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer(), rp.OAuthConfig().Endpoint.TokenURL}, time.Hour, rp.Signer())
|
||||
if err != nil {
|
||||
unauthorizedError(w, r, "failed to build assertion: "+err.Error(), state, rp)
|
||||
return
|
||||
}
|
||||
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
|
||||
}
|
||||
tokens, err := CodeExchange[C](r.Context(), params.Get("code"), rp, codeOpts...)
|
||||
tokens, err := CodeExchange[C](r.Context(), r.FormValue("code"), rp, codeOpts...)
|
||||
if err != nil {
|
||||
unauthorizedError(w, r, "failed to exchange token: "+err.Error(), state, rp)
|
||||
return
|
||||
|
@ -541,6 +568,10 @@ type CodeExchangeUserinfoCallback[C oidc.IDClaims, U SubjectGetter] func(w http.
|
|||
// on success it will pass the userinfo into its callback function as well
|
||||
func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCallback[C, U]) CodeExchangeCallback[C] {
|
||||
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) {
|
||||
ctx, span := client.Tracer.Start(r.Context(), "UserinfoCallback")
|
||||
r = r.WithContext(ctx)
|
||||
defer span.End()
|
||||
|
||||
info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
||||
if err != nil {
|
||||
unauthorizedError(w, r, "userinfo failed: "+err.Error(), state, rp)
|
||||
|
@ -559,6 +590,8 @@ func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCa
|
|||
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 {
|
||||
|
@ -622,7 +655,7 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
|
|||
}
|
||||
}
|
||||
|
||||
// withURLParam sets custom url paramaters.
|
||||
// withURLParam sets custom url parameters.
|
||||
// This is the generalized, unexported, function used by both
|
||||
// URLParamOpt and AuthURLOpt.
|
||||
func withURLParam(key, value string) func() []oauth2.AuthCodeOption {
|
||||
|
@ -701,11 +734,11 @@ func (t tokenEndpointCaller) TokenEndpoint() string {
|
|||
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `schema:"refresh_token"`
|
||||
Scopes oidc.SpaceDelimitedArray `schema:"scope"`
|
||||
ClientID string `schema:"client_id"`
|
||||
ClientSecret string `schema:"client_secret"`
|
||||
ClientAssertion string `schema:"client_assertion"`
|
||||
ClientAssertionType string `schema:"client_assertion_type"`
|
||||
Scopes oidc.SpaceDelimitedArray `schema:"scope,omitempty"`
|
||||
ClientID string `schema:"client_id,omitempty"`
|
||||
ClientSecret string `schema:"client_secret,omitempty"`
|
||||
ClientAssertion string `schema:"client_assertion,omitempty"`
|
||||
ClientAssertionType string `schema:"client_assertion_type,omitempty"`
|
||||
GrantType oidc.GrantType `schema:"grant_type"`
|
||||
}
|
||||
|
||||
|
@ -714,9 +747,12 @@ type RefreshTokenRequest struct {
|
|||
// 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 verfied
|
||||
// the IDToken and AccessToken will be verified
|
||||
// and the IDToken and IDTokenClaims fields will be populated in the returned object.
|
||||
func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refreshToken, clientAssertion, clientAssertionType string) (*oidc.Tokens[C], error) {
|
||||
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
|
||||
defer span.End()
|
||||
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "RefreshTokens")
|
||||
request := RefreshTokenRequest{
|
||||
RefreshToken: refreshToken,
|
||||
|
@ -742,6 +778,9 @@ func RefreshTokens[C oidc.IDClaims](ctx context.Context, rp RelyingParty, refres
|
|||
|
||||
func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) {
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "EndSession")
|
||||
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
|
||||
defer span.End()
|
||||
|
||||
request := oidc.EndSessionRequest{
|
||||
IdTokenHint: idToken,
|
||||
ClientID: rp.OAuthConfig().ClientID,
|
||||
|
@ -758,6 +797,8 @@ func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectU
|
|||
// tokenTypeHint should be either "id_token" or "refresh_token".
|
||||
func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHint string) error {
|
||||
ctx = logCtxWithRPData(ctx, rp, "function", "RevokeToken")
|
||||
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
|
||||
defer span.End()
|
||||
request := client.RevokeRequest{
|
||||
Token: token,
|
||||
TokenTypeHint: tokenTypeHint,
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"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"
|
||||
tu "github.com/zitadel/oidc/v3/internal/testutil"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc/grants/tokenexchange"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc/grants/tokenexchange"
|
||||
)
|
||||
|
||||
// TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
|
|
|
@ -4,14 +4,18 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
// VerifyTokens implement the Token Response Validation as defined in OIDC specification
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
|
||||
func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v *IDTokenVerifier) (claims C, err error) {
|
||||
ctx, span := client.Tracer.Start(ctx, "VerifyTokens")
|
||||
defer span.End()
|
||||
|
||||
var nilClaims C
|
||||
|
||||
claims, err = VerifyIDToken[C](ctx, idToken, v)
|
||||
|
@ -27,6 +31,9 @@ func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken str
|
|||
// VerifyIDToken validates the id token according to
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||
func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenVerifier) (claims C, err error) {
|
||||
ctx, span := client.Tracer.Start(ctx, "VerifyIDToken")
|
||||
defer span.End()
|
||||
|
||||
var nilClaims C
|
||||
|
||||
decrypted, err := oidc.DecryptToken(token)
|
||||
|
@ -66,8 +73,10 @@ func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenV
|
|||
return nilClaims, err
|
||||
}
|
||||
|
||||
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
|
||||
return nilClaims, err
|
||||
if v.Nonce != nil {
|
||||
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
|
||||
return nilClaims, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil {
|
||||
|
|
|
@ -5,11 +5,11 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
tu "github.com/zitadel/oidc/v3/internal/testutil"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
func TestVerifyTokens(t *testing.T) {
|
||||
|
@ -100,22 +100,21 @@ func TestVerifyIDToken(t *testing.T) {
|
|||
MaxAge: 2 * time.Minute,
|
||||
ACR: tu.ACRVerify,
|
||||
Nonce: func(context.Context) string { return tu.ValidNonce },
|
||||
ClientID: tu.ValidClientID,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
clientID string
|
||||
tokenClaims func() (string, *oidc.IDTokenClaims)
|
||||
wantErr bool
|
||||
name string
|
||||
tokenClaims func() (string, *oidc.IDTokenClaims)
|
||||
customVerifier func(verifier *IDTokenVerifier)
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
clientID: tu.ValidClientID,
|
||||
tokenClaims: tu.ValidIDToken,
|
||||
},
|
||||
{
|
||||
name: "custom claims",
|
||||
clientID: tu.ValidClientID,
|
||||
name: "custom claims",
|
||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||
return tu.NewIDTokenCustom(
|
||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||
|
@ -125,21 +124,31 @@ func TestVerifyIDToken(t *testing.T) {
|
|||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
clientID: tu.ValidClientID,
|
||||
tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil },
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid signature",
|
||||
clientID: tu.ValidClientID,
|
||||
tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil },
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty subject",
|
||||
clientID: tu.ValidClientID,
|
||||
name: "empty subject",
|
||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||
return tu.NewIDToken(
|
||||
tu.ValidIssuer, "", tu.ValidAudience,
|
||||
|
@ -150,8 +159,7 @@ func TestVerifyIDToken(t *testing.T) {
|
|||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong issuer",
|
||||
clientID: tu.ValidClientID,
|
||||
name: "wrong issuer",
|
||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||
return tu.NewIDToken(
|
||||
"foo", tu.ValidSubject, tu.ValidAudience,
|
||||
|
@ -162,14 +170,15 @@ func TestVerifyIDToken(t *testing.T) {
|
|||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong clientID",
|
||||
clientID: "foo",
|
||||
name: "wrong clientID",
|
||||
customVerifier: func(verifier *IDTokenVerifier) {
|
||||
verifier.ClientID = "foo"
|
||||
},
|
||||
tokenClaims: tu.ValidIDToken,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "expired",
|
||||
clientID: tu.ValidClientID,
|
||||
name: "expired",
|
||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||
return tu.NewIDToken(
|
||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||
|
@ -180,8 +189,7 @@ func TestVerifyIDToken(t *testing.T) {
|
|||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong IAT",
|
||||
clientID: tu.ValidClientID,
|
||||
name: "wrong IAT",
|
||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||
return tu.NewIDToken(
|
||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||
|
@ -192,8 +200,7 @@ func TestVerifyIDToken(t *testing.T) {
|
|||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong acr",
|
||||
clientID: tu.ValidClientID,
|
||||
name: "wrong acr",
|
||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||
return tu.NewIDToken(
|
||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||
|
@ -204,8 +211,7 @@ func TestVerifyIDToken(t *testing.T) {
|
|||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "expired auth",
|
||||
clientID: tu.ValidClientID,
|
||||
name: "expired auth",
|
||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||
return tu.NewIDToken(
|
||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||
|
@ -216,8 +222,7 @@ func TestVerifyIDToken(t *testing.T) {
|
|||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong nonce",
|
||||
clientID: tu.ValidClientID,
|
||||
name: "wrong nonce",
|
||||
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||
return tu.NewIDToken(
|
||||
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||
|
@ -231,7 +236,10 @@ func TestVerifyIDToken(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
token, want := tt.tokenClaims()
|
||||
verifier.ClientID = tt.clientID
|
||||
if tt.customVerifier != nil {
|
||||
tt.customVerifier(verifier)
|
||||
}
|
||||
|
||||
got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
tu "github.com/zitadel/oidc/v3/internal/testutil"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
// MyCustomClaims extends the TokenClaims base,
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rs"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
type IntrospectionResponse struct {
|
||||
|
|
|
@ -6,9 +6,9 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
type ResourceServer interface {
|
||||
|
@ -123,6 +123,9 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option {
|
|||
//
|
||||
// [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")
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
func TestNewResourceServer(t *testing.T) {
|
||||
|
@ -201,7 +201,7 @@ func TestIntrospect(t *testing.T) {
|
|||
{
|
||||
name: "missing-introspect-url",
|
||||
args: args{
|
||||
ctx: nil,
|
||||
ctx: context.Background(),
|
||||
rp: rp,
|
||||
token: "my-token",
|
||||
},
|
||||
|
|
|
@ -4,10 +4,12 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
type TokenExchanger interface {
|
||||
|
@ -33,6 +35,17 @@ func NewTokenExchangerClientCredentials(ctx context.Context, issuer, clientID, c
|
|||
return newOAuthTokenExchange(ctx, issuer, authorizer, options...)
|
||||
}
|
||||
|
||||
func NewTokenExchangerJWTProfile(ctx context.Context, issuer, clientID string, signer jose.Signer, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
|
||||
authorizer := func() (any, error) {
|
||||
assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ClientAssertionFormAuthorization(assertion), nil
|
||||
}
|
||||
return newOAuthTokenExchange(ctx, issuer, authorizer, options...)
|
||||
}
|
||||
|
||||
func newOAuthTokenExchange(ctx context.Context, issuer string, authorizer func() (any, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) {
|
||||
te := &OAuthTokenExchange{
|
||||
httpClient: httphelper.DefaultHTTPClient,
|
||||
|
@ -101,6 +114,9 @@ func ExchangeToken(
|
|||
Scopes []string,
|
||||
RequestedTokenType oidc.TokenType,
|
||||
) (*oidc.TokenExchangeResponse, error) {
|
||||
ctx, span := client.Tracer.Start(ctx, "ExchangeToken")
|
||||
defer span.End()
|
||||
|
||||
if SubjectToken == "" {
|
||||
return nil, errors.New("empty subject_token")
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"fmt"
|
||||
"hash"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
var ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm")
|
||||
|
@ -21,6 +21,14 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) {
|
|||
return sha512.New384(), nil
|
||||
case jose.RS512, jose.ES512, jose.PS512:
|
||||
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:
|
||||
return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm)
|
||||
}
|
||||
|
|
|
@ -1,22 +1,45 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
func BytesToPrivateKey(b []byte) (*rsa.PrivateKey, error) {
|
||||
var (
|
||||
ErrPEMDecode = errors.New("PEM decode failed")
|
||||
ErrUnsupportedFormat = errors.New("key is neither in PKCS#1 nor PKCS#8 format")
|
||||
ErrUnsupportedPrivateKey = errors.New("unsupported key type, must be RSA, ECDSA or ED25519 private key")
|
||||
)
|
||||
|
||||
func BytesToPrivateKey(b []byte) (crypto.PublicKey, jose.SignatureAlgorithm, error) {
|
||||
block, _ := pem.Decode(b)
|
||||
if block == nil {
|
||||
return nil, errors.New("PEM decode failed")
|
||||
return nil, "", ErrPEMDecode
|
||||
}
|
||||
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err == nil {
|
||||
return privateKey, jose.RS256, nil
|
||||
}
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", ErrUnsupportedFormat
|
||||
}
|
||||
switch privateKey := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return privateKey, jose.RS256, nil
|
||||
case ed25519.PrivateKey:
|
||||
return privateKey, jose.EdDSA, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return privateKey, jose.ES256, nil
|
||||
default:
|
||||
return nil, "", ErrUnsupportedPrivateKey
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
|
|
@ -1,21 +1,64 @@
|
|||
package crypto_test
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
zcrypto "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
|
||||
)
|
||||
|
||||
func TestBytesToPrivateKey(tt *testing.T) {
|
||||
tt.Run("PEMDecodeError", func(t *testing.T) {
|
||||
_, err := crypto.BytesToPrivateKey([]byte("The non-PEM sequence"))
|
||||
assert.EqualError(t, err, "PEM decode failed")
|
||||
})
|
||||
|
||||
tt.Run("InvalidKeyFormat", func(t *testing.T) {
|
||||
_, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
|
@ -42,21 +85,50 @@ srJnjF0H8oKmAY6hw+1Tm/n/b08p+RyL48TgVSE2vhUCgYA3BWpkD4PlCcn/FZsq
|
|||
OrLFyFXI6jIaxskFtsRW1IxxIlAdZmxfB26P/2gx6VjLdxJI/RRPkJyEN2dP7CbR
|
||||
BDjb565dy1O9D6+UrY70Iuwjz+OcALRBBGTaiF2pLn6IhSzNI2sy/tXX8q8dBlg9
|
||||
OFCrqT/emes3KytTPfa5NZtYeQ==
|
||||
-----END PRIVATE KEY-----`))
|
||||
assert.EqualError(t, err, "x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)")
|
||||
})
|
||||
-----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)
|
||||
})
|
||||
|
||||
tt.Run("Ok", func(t *testing.T) {
|
||||
key, err := crypto.BytesToPrivateKey([]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-----`))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
func Sign(object any, signer jose.Signer) (string, error) {
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
var DefaultHTTPClient = &http.Client{
|
||||
|
@ -66,7 +68,12 @@ func HttpRequest(client *http.Client, req *http.Request, response any) error {
|
|||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("http status not ok: %s %s", resp.Status, body)
|
||||
var oidcErr oidc.Error
|
||||
err = json.Unmarshal(body, &oidcErr)
|
||||
if err != nil || oidcErr.ErrorType == "" {
|
||||
return fmt.Errorf("http status not ok: %s %s", resp.Status, body)
|
||||
}
|
||||
return &oidcErr
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, response)
|
||||
|
|
|
@ -48,6 +48,7 @@ const (
|
|||
|
||||
ResponseModeQuery ResponseMode = "query"
|
||||
ResponseModeFragment ResponseMode = "fragment"
|
||||
ResponseModeFormPost ResponseMode = "form_post"
|
||||
|
||||
// PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages.
|
||||
// An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed
|
||||
|
|
|
@ -3,7 +3,7 @@ package oidc
|
|||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package oidc
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// DeviceAuthorizationRequest implements
|
||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1,
|
||||
// 3.1 Device Authorization Request.
|
||||
|
@ -20,6 +22,26 @@ type DeviceAuthorizationResponse struct {
|
|||
Interval int `json:"interval,omitempty"`
|
||||
}
|
||||
|
||||
func (resp *DeviceAuthorizationResponse) UnmarshalJSON(data []byte) error {
|
||||
type Alias DeviceAuthorizationResponse
|
||||
aux := &struct {
|
||||
// workaround misspelling of verification_uri
|
||||
// https://stackoverflow.com/q/76696956/5690223
|
||||
// https://developers.google.com/identity/protocols/oauth2/limited-input-device?hl=fr#success-response
|
||||
VerificationURL string `json:"verification_url"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(resp),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.VerificationURI == "" {
|
||||
resp.VerificationURI = aux.VerificationURL
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeviceAccessTokenRequest implements
|
||||
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4,
|
||||
// Device Access Token Request.
|
||||
|
|
30
pkg/oidc/device_authorization_test.go
Normal file
30
pkg/oidc/device_authorization_test.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
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)
|
||||
}
|
|
@ -145,6 +145,14 @@ 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 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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
@ -27,6 +28,11 @@ const (
|
|||
SlowDown errorType = "slow_down"
|
||||
AccessDenied errorType = "access_denied"
|
||||
ExpiredToken errorType = "expired_token"
|
||||
|
||||
// InvalidTarget error is returned by Token Exchange if
|
||||
// the requested target or audience is invalid.
|
||||
// [RFC 8693, Section 2.2.2: Error Response](https://www.rfc-editor.org/rfc/rfc8693#section-2.2.2)
|
||||
InvalidTarget errorType = "invalid_target"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -112,6 +118,14 @@ var (
|
|||
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 {
|
||||
|
@ -119,7 +133,28 @@ type Error struct {
|
|||
ErrorType errorType `json:"error" schema:"error"`
|
||||
Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
|
||||
State string `json:"state,omitempty" schema:"state,omitempty"`
|
||||
SessionState string `json:"session_state,omitempty" schema:"session_state,omitempty"`
|
||||
redirectDisabled bool `schema:"-"`
|
||||
returnParent bool `schema:"-"`
|
||||
}
|
||||
|
||||
func (e *Error) MarshalJSON() ([]byte, error) {
|
||||
m := struct {
|
||||
Error errorType `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
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 {
|
||||
|
@ -144,7 +179,8 @@ func (e *Error) Is(target error) bool {
|
|||
}
|
||||
return e.ErrorType == t.ErrorType &&
|
||||
(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 {
|
||||
|
@ -152,6 +188,18 @@ func (e *Error) WithParent(err error) *Error {
|
|||
return e
|
||||
}
|
||||
|
||||
// WithReturnParentToClient allows returning the set parent error to the HTTP client.
|
||||
// Currently it only supports setting the parent inside JSON responses, not redirect URLs.
|
||||
// As Go errors don't unmarshal well, only the marshaller is implemented for the moment.
|
||||
//
|
||||
// Warning: parent errors may contain sensitive data or unwanted details about the server status.
|
||||
// Also, the `parent` field is not a standard error field and might confuse certain clients
|
||||
// that require fully compliant responses.
|
||||
func (e *Error) WithReturnParentToClient(b bool) *Error {
|
||||
e.returnParent = b
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Error) WithDescription(desc string, args ...any) *Error {
|
||||
e.Description = fmt.Sprintf(desc, args...)
|
||||
return e
|
||||
|
@ -198,6 +246,9 @@ func (e *Error) LogValue() slog.Value {
|
|||
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))
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
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) {
|
||||
|
@ -151,3 +154,39 @@ func TestError_LogValue(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
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,18 +16,21 @@ type ClientAssertionParams struct {
|
|||
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims.
|
||||
type IntrospectionResponse struct {
|
||||
Active bool `json:"active"`
|
||||
Scope SpaceDelimitedArray `json:"scope,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
Expiration Time `json:"exp,omitempty"`
|
||||
IssuedAt Time `json:"iat,omitempty"`
|
||||
NotBefore Time `json:"nbf,omitempty"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
Audience Audience `json:"aud,omitempty"`
|
||||
Issuer string `json:"iss,omitempty"`
|
||||
JWTID string `json:"jti,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
Scope SpaceDelimitedArray `json:"scope,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
Expiration Time `json:"exp,omitempty"`
|
||||
IssuedAt Time `json:"iat,omitempty"`
|
||||
AuthTime Time `json:"auth_time,omitempty"`
|
||||
NotBefore Time `json:"nbf,omitempty"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
Audience Audience `json:"aud,omitempty"`
|
||||
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
||||
Issuer string `json:"iss,omitempty"`
|
||||
JWTID string `json:"jti,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Actor *ActorClaims `json:"act,omitempty"`
|
||||
UserInfoProfile
|
||||
UserInfoEmail
|
||||
UserInfoPhone
|
||||
|
|
|
@ -6,8 +6,9 @@ import (
|
|||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -92,17 +93,17 @@ func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (k
|
|||
}
|
||||
|
||||
func algToKeyType(key any, alg string) bool {
|
||||
switch alg[0] {
|
||||
case 'R', 'P':
|
||||
if strings.HasPrefix(alg, "RS") || strings.HasPrefix(alg, "PS") {
|
||||
_, ok := key.(*rsa.PublicKey)
|
||||
return ok
|
||||
case 'E':
|
||||
}
|
||||
if strings.HasPrefix(alg, "ES") {
|
||||
_, ok := key.(*ecdsa.PublicKey)
|
||||
return ok
|
||||
case 'O':
|
||||
_, ok := key.(*ed25519.PublicKey)
|
||||
return ok
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if alg == string(jose.EdDSA) {
|
||||
_, ok := key.(ed25519.PublicKey)
|
||||
return ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
func TestFindKey(t *testing.T) {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package oidc
|
||||
|
||||
// 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 {
|
||||
IdTokenHint string `schema:"id_token_hint"`
|
||||
ClientID string `schema:"client_id"`
|
||||
PostLogoutRedirectURI string `schema:"post_logout_redirect_uri"`
|
||||
State string `schema:"state"`
|
||||
IdTokenHint string `schema:"id_token_hint"`
|
||||
LogoutHint string `schema:"logout_hint"`
|
||||
ClientID string `schema:"client_id"`
|
||||
PostLogoutRedirectURI string `schema:"post_logout_redirect_uri"`
|
||||
State string `schema:"state"`
|
||||
UILocales Locales `schema:"ui_locales"`
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ import (
|
|||
"os"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -34,19 +35,20 @@ type Tokens[C IDClaims] struct {
|
|||
// TokenClaims implements the Claims interface,
|
||||
// and can be used to extend larger claim types by embedding.
|
||||
type TokenClaims struct {
|
||||
Issuer string `json:"iss,omitempty"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
Audience Audience `json:"aud,omitempty"`
|
||||
Expiration Time `json:"exp,omitempty"`
|
||||
IssuedAt Time `json:"iat,omitempty"`
|
||||
AuthTime Time `json:"auth_time,omitempty"`
|
||||
NotBefore Time `json:"nbf,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
AuthenticationContextClassReference string `json:"acr,omitempty"`
|
||||
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
||||
AuthorizedParty string `json:"azp,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
JWTID string `json:"jti,omitempty"`
|
||||
Issuer string `json:"iss,omitempty"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
Audience Audience `json:"aud,omitempty"`
|
||||
Expiration Time `json:"exp,omitempty"`
|
||||
IssuedAt Time `json:"iat,omitempty"`
|
||||
AuthTime Time `json:"auth_time,omitempty"`
|
||||
NotBefore Time `json:"nbf,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
AuthenticationContextClassReference string `json:"acr,omitempty"`
|
||||
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
||||
AuthorizedParty string `json:"azp,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
JWTID string `json:"jti,omitempty"`
|
||||
Actor *ActorClaims `json:"act,omitempty"`
|
||||
|
||||
// Additional information set by this framework
|
||||
SignatureAlg jose.SignatureAlgorithm `json:"-"`
|
||||
|
@ -115,6 +117,7 @@ func NewAccessTokenClaims(issuer, subject string, audience []string, expiration
|
|||
Expiration: FromTime(expiration),
|
||||
IssuedAt: FromTime(now),
|
||||
NotBefore: FromTime(now),
|
||||
ClientID: clientID,
|
||||
JWTID: jwtid,
|
||||
},
|
||||
}
|
||||
|
@ -204,13 +207,36 @@ func (i *IDTokenClaims) UnmarshalJSON(data []byte) error {
|
|||
return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims)
|
||||
}
|
||||
|
||||
// ActorClaims provides the `act` claims used for impersonation or delegation Token Exchange.
|
||||
//
|
||||
// An actor can be nested in case an obtained token is used as actor token to obtain impersonation or delegation.
|
||||
// This allows creating a chain of actors.
|
||||
// See [RFC 8693, section 4.1](https://www.rfc-editor.org/rfc/rfc8693#name-act-actor-claim).
|
||||
type ActorClaims struct {
|
||||
Actor *ActorClaims `json:"act,omitempty"`
|
||||
Issuer string `json:"iss,omitempty"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
Claims map[string]any `json:"-"`
|
||||
}
|
||||
|
||||
type acAlias ActorClaims
|
||||
|
||||
func (c *ActorClaims) MarshalJSON() ([]byte, error) {
|
||||
return mergeAndMarshalClaims((*acAlias)(c), c.Claims)
|
||||
}
|
||||
|
||||
func (c *ActorClaims) UnmarshalJSON(data []byte) error {
|
||||
return unmarshalJSONMulti(data, (*acAlias)(c), &c.Claims)
|
||||
}
|
||||
|
||||
type AccessTokenResponse struct {
|
||||
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
|
||||
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
|
||||
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
|
||||
State string `json:"state,omitempty" schema:"state,omitempty"`
|
||||
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
|
||||
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
|
||||
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
|
||||
State string `json:"state,omitempty" schema:"state,omitempty"`
|
||||
Scope SpaceDelimitedArray `json:"scope,omitempty" schema:"scope,omitempty"`
|
||||
}
|
||||
|
||||
type JWTProfileAssertionClaims struct {
|
||||
|
@ -321,12 +347,12 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
|
|||
}
|
||||
|
||||
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) {
|
||||
privateKey, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
|
||||
privateKey, algorithm, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
key := jose.SigningKey{
|
||||
Algorithm: jose.RS256,
|
||||
Algorithm: algorithm,
|
||||
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
|
||||
}
|
||||
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
||||
|
@ -352,4 +378,45 @@ type TokenExchangeResponse struct {
|
|||
ExpiresIn uint64 `json:"expires_in,omitempty"`
|
||||
Scopes SpaceDelimitedArray `json:"scope,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
|
||||
// IDToken field allows returning an additional ID token
|
||||
// if the requested_token_type was Access Token and scope contained openid.
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
}
|
||||
|
||||
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,9 +3,10 @@ package oidc
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -57,13 +58,7 @@ var AllTokenTypes = []TokenType{
|
|||
type TokenType string
|
||||
|
||||
func (t TokenType) IsSupported() bool {
|
||||
for _, tt := range AllTokenTypes {
|
||||
if t == tt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return slices.Contains(AllTokenTypes, t)
|
||||
}
|
||||
|
||||
type TokenRequest interface {
|
||||
|
@ -77,10 +72,10 @@ type AccessTokenRequest struct {
|
|||
Code string `schema:"code"`
|
||||
RedirectURI string `schema:"redirect_uri"`
|
||||
ClientID string `schema:"client_id"`
|
||||
ClientSecret string `schema:"client_secret"`
|
||||
CodeVerifier string `schema:"code_verifier"`
|
||||
ClientAssertion string `schema:"client_assertion"`
|
||||
ClientAssertionType string `schema:"client_assertion_type"`
|
||||
ClientSecret string `schema:"client_secret,omitempty"`
|
||||
CodeVerifier string `schema:"code_verifier,omitempty"`
|
||||
ClientAssertion string `schema:"client_assertion,omitempty"`
|
||||
ClientAssertionType string `schema:"client_assertion_type,omitempty"`
|
||||
}
|
||||
|
||||
func (a *AccessTokenRequest) GrantType() GrantType {
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
@ -145,6 +145,7 @@ func TestNewAccessTokenClaims(t *testing.T) {
|
|||
Subject: "hello@me.com",
|
||||
Audience: Audience{"foo"},
|
||||
Expiration: 12345,
|
||||
ClientID: "foo",
|
||||
JWTID: "900",
|
||||
},
|
||||
}
|
||||
|
@ -241,3 +242,39 @@ func TestIDTokenClaims_GetUserInfo(t *testing.T) {
|
|||
got := idTokenData.GetUserInfo()
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func TestNewLogoutTokenClaims(t *testing.T) {
|
||||
want := &LogoutTokenClaims{
|
||||
Issuer: "zitadel",
|
||||
Subject: "hello@me.com",
|
||||
Audience: Audience{"foo", "just@me.com"},
|
||||
Expiration: 12345,
|
||||
JWTID: "jwtID",
|
||||
Events: map[string]any{
|
||||
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
||||
},
|
||||
SessionID: "sessionID",
|
||||
Claims: nil,
|
||||
}
|
||||
|
||||
got := NewLogoutTokenClaims(
|
||||
want.Issuer,
|
||||
want.Subject,
|
||||
want.Audience,
|
||||
want.Expiration.AsTime(),
|
||||
want.JWTID,
|
||||
want.SessionID,
|
||||
1*time.Second,
|
||||
)
|
||||
|
||||
// test if the dynamic timestamp is around now,
|
||||
// allowing for a delta of 1, just in case we flip on
|
||||
// either side of a second boundry.
|
||||
nowMinusSkew := NowTime() - 1
|
||||
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
|
||||
|
||||
// Make equal not fail on dynamic timestamp
|
||||
got.IssuedAt = 0
|
||||
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/zitadel/schema"
|
||||
"golang.org/x/text/language"
|
||||
|
@ -35,6 +35,17 @@ func (a *Audience) UnmarshalJSON(text []byte) error {
|
|||
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
|
||||
|
||||
func (d *Display) UnmarshalText(text []byte) error {
|
||||
|
@ -82,6 +93,9 @@ func (l *Locale) MarshalJSON() ([]byte, error) {
|
|||
// to an empty value (language "und") and no error will be returned.
|
||||
// This state can be checked with the `l.Tag().IsRoot()` method.
|
||||
func (l *Locale) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 || string(data) == "\"\"" {
|
||||
return nil
|
||||
}
|
||||
err := json.Unmarshal(data, &l.tag)
|
||||
if err == nil {
|
||||
return nil
|
||||
|
@ -112,6 +126,14 @@ func ParseLocales(locales []string) Locales {
|
|||
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
|
||||
|
@ -228,6 +250,9 @@ func NewEncoder() *schema.Encoder {
|
|||
e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string {
|
||||
return value.Interface().(SpaceDelimitedArray).String()
|
||||
})
|
||||
e.RegisterEncoder(Locales{}, func(value reflect.Value) string {
|
||||
return value.Interface().(Locales).String()
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
|
|
|
@ -217,6 +217,30 @@ func TestLocale_UnmarshalJSON(t *testing.T) {
|
|||
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"}`,
|
||||
|
@ -237,16 +261,17 @@ func TestLocale_UnmarshalJSON(t *testing.T) {
|
|||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
var got dst
|
||||
err := json.Unmarshal([]byte(tt.input), &got)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,12 +7,11 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
|
||||
str "github.com/zitadel/oidc/v3/pkg/strings"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
type Claims interface {
|
||||
|
@ -41,6 +40,7 @@ type IDClaims interface {
|
|||
var (
|
||||
ErrParse = errors.New("parsing of request failed")
|
||||
ErrIssuerInvalid = errors.New("issuer does not match")
|
||||
ErrDiscoveryFailed = errors.New("OpenID Provider Configuration Discovery has failed")
|
||||
ErrSubjectMissing = errors.New("subject missing")
|
||||
ErrAudience = errors.New("audience is not valid")
|
||||
ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty")
|
||||
|
@ -83,7 +83,7 @@ type ACRVerifier func(string) error
|
|||
// if none of the provided values matches the acr claim
|
||||
func DefaultACRVerifier(possibleValues []string) ACRVerifier {
|
||||
return func(acr string) error {
|
||||
if !str.Contains(possibleValues, acr) {
|
||||
if !slices.Contains(possibleValues, acr) {
|
||||
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
|
||||
}
|
||||
return nil
|
||||
|
@ -122,7 +122,7 @@ func CheckIssuer(claims Claims, issuer string) error {
|
|||
}
|
||||
|
||||
func CheckAudience(claims Claims, clientID string) error {
|
||||
if !str.Contains(claims.GetAudience(), clientID) {
|
||||
if !slices.Contains(claims.GetAudience(), clientID) {
|
||||
return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID)
|
||||
}
|
||||
|
||||
|
@ -148,8 +148,13 @@ func CheckAuthorizedParty(claims Claims, clientID string) error {
|
|||
}
|
||||
|
||||
func CheckSignature(ctx context.Context, token string, payload []byte, claims ClaimsSignature, supportedSigAlgs []string, set KeySet) error {
|
||||
jws, err := jose.ParseSigned(token)
|
||||
jws, err := jose.ParseSigned(token, toJoseSignatureAlgorithms(supportedSigAlgs))
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "go-jose/go-jose: unexpected signature algorithm") {
|
||||
// TODO(v4): we should wrap errors instead of returning static ones.
|
||||
// This is a workaround so we keep returning the same error for now.
|
||||
return ErrSignatureUnsupportedAlg
|
||||
}
|
||||
return ErrParse
|
||||
}
|
||||
if len(jws.Signatures) == 0 {
|
||||
|
@ -159,12 +164,6 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl
|
|||
return ErrSignatureMultiple
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
|
@ -180,6 +179,18 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO(v4): Use the new jose.SignatureAlgorithm type directly, instead of string.
|
||||
func toJoseSignatureAlgorithms(algorithms []string) []jose.SignatureAlgorithm {
|
||||
out := make([]jose.SignatureAlgorithm, len(algorithms))
|
||||
for i := range algorithms {
|
||||
out[i] = jose.SignatureAlgorithm(algorithms[i])
|
||||
}
|
||||
if len(out) == 0 {
|
||||
out = append(out, jose.RS256, jose.ES256, jose.PS256)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func CheckExpiration(claims Claims, offset time.Duration) error {
|
||||
expiration := claims.GetExpiration()
|
||||
if !time.Now().Add(offset).Before(expiration) {
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"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"
|
||||
tu "github.com/zitadel/oidc/v3/internal/testutil"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
func TestParseToken(t *testing.T) {
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
package op
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
str "github.com/zitadel/oidc/v3/pkg/strings"
|
||||
)
|
||||
|
||||
type AuthRequest interface {
|
||||
|
@ -35,6 +38,13 @@ type AuthRequest interface {
|
|||
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 {
|
||||
Storage() Storage
|
||||
Decoder() httphelper.Decoder
|
||||
|
@ -52,13 +62,19 @@ type AuthorizeValidator interface {
|
|||
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) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
Authorize(w, r, authorizer)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
AuthorizeCallback(w, r, authorizer)
|
||||
}
|
||||
|
@ -67,30 +83,41 @@ func authorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, *
|
|||
// Authorize handles the authorization request, including
|
||||
// parsing, validating, storing and finally redirecting to the login handler
|
||||
func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||
ctx, span := tracer.Start(r.Context(), "Authorize")
|
||||
r = r.WithContext(ctx)
|
||||
defer span.End()
|
||||
|
||||
authReq, err := ParseAuthorizeRequest(r, authorizer.Decoder())
|
||||
if err != nil {
|
||||
AuthRequestError(w, r, nil, err, authorizer)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
|
||||
err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
|
||||
if err != nil {
|
||||
AuthRequestError(w, r, authReq, err, authorizer)
|
||||
AuthRequestError(w, r, nil, err, authorizer)
|
||||
return
|
||||
}
|
||||
}
|
||||
if authReq.ClientID == "" {
|
||||
AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing client_id"), authorizer)
|
||||
AuthRequestError(w, r, nil, fmt.Errorf("auth request is missing client_id"), authorizer)
|
||||
return
|
||||
}
|
||||
if authReq.RedirectURI == "" {
|
||||
AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing redirect_uri"), authorizer)
|
||||
AuthRequestError(w, r, nil, fmt.Errorf("auth request is missing redirect_uri"), authorizer)
|
||||
return
|
||||
}
|
||||
validation := ValidateAuthRequest
|
||||
if validater, ok := authorizer.(AuthorizeValidator); ok {
|
||||
validation = validater.ValidateAuthRequest
|
||||
|
||||
var client Client
|
||||
validation := func(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) {
|
||||
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))
|
||||
if err != nil {
|
||||
|
@ -106,11 +133,6 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
|||
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer)
|
||||
return
|
||||
}
|
||||
client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID())
|
||||
if err != nil {
|
||||
AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer)
|
||||
return
|
||||
}
|
||||
RedirectToLogin(req.GetID(), client, w, r)
|
||||
}
|
||||
|
||||
|
@ -146,7 +168,7 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage
|
|||
if requestObject.Issuer != requestObject.ClientID {
|
||||
return oidc.ErrInvalidRequest().WithDescription("missing or wrong issuer in request")
|
||||
}
|
||||
if !str.Contains(requestObject.Audience, issuer) {
|
||||
if !slices.Contains(requestObject.Audience, issuer) {
|
||||
return oidc.ErrInvalidRequest().WithDescription("issuer missing in audience")
|
||||
}
|
||||
keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer}
|
||||
|
@ -160,7 +182,7 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage
|
|||
// CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request
|
||||
// and clears the `RequestParam` of the auth request
|
||||
func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) {
|
||||
if str.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 {
|
||||
if slices.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 {
|
||||
authReq.Scopes = requestObject.Scopes
|
||||
}
|
||||
if requestObject.RedirectURI != "" {
|
||||
|
@ -205,23 +227,37 @@ func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oi
|
|||
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.
|
||||
//
|
||||
// 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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
client, err := storage.GetClientByClientID(ctx, authReq.ClientID)
|
||||
if err != nil {
|
||||
return "", oidc.DefaultToServerError(err, "unable to retrieve client by id")
|
||||
}
|
||||
authReq.Scopes, err = ValidateAuthReqScopes(client, authReq.Scopes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := ValidateAuthReqResponseType(client, authReq.ResponseType); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -241,44 +277,30 @@ func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error)
|
|||
return maxAge, nil
|
||||
}
|
||||
|
||||
// ValidateAuthReqScopes validates the passed scopes
|
||||
// ValidateAuthReqScopes validates the passed scopes and deletes any unsupported scopes.
|
||||
// An error is returned if scopes is empty.
|
||||
func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) {
|
||||
if len(scopes) == 0 {
|
||||
return nil, oidc.ErrInvalidRequest().
|
||||
WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " +
|
||||
"If you have any questions, you may contact the administrator of the application.")
|
||||
}
|
||||
openID := false
|
||||
for i := len(scopes) - 1; i >= 0; i-- {
|
||||
scope := scopes[i]
|
||||
if scope == oidc.ScopeOpenID {
|
||||
openID = true
|
||||
continue
|
||||
}
|
||||
if !(scope == oidc.ScopeProfile ||
|
||||
scopes = slices.DeleteFunc(scopes, func(scope string) bool {
|
||||
return !(scope == oidc.ScopeOpenID ||
|
||||
scope == oidc.ScopeProfile ||
|
||||
scope == oidc.ScopeEmail ||
|
||||
scope == oidc.ScopePhone ||
|
||||
scope == oidc.ScopeAddress ||
|
||||
scope == oidc.ScopeOfflineAccess) &&
|
||||
!client.IsScopeAllowed(scope) {
|
||||
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.")
|
||||
}
|
||||
|
||||
!client.IsScopeAllowed(scope)
|
||||
})
|
||||
return scopes, nil
|
||||
}
|
||||
|
||||
// checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores
|
||||
// other factors.
|
||||
func checkURIAgainstRedirects(client Client, uri string) error {
|
||||
if str.Contains(client.RedirectURIs(), uri) {
|
||||
if slices.Contains(client.RedirectURIs(), uri) {
|
||||
return nil
|
||||
}
|
||||
if globClient, ok := client.(HasRedirectGlobs); ok {
|
||||
|
@ -303,12 +325,12 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res
|
|||
return oidc.ErrInvalidRequestRedirectURI().WithDescription("The redirect_uri is missing in the request. " +
|
||||
"Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.")
|
||||
}
|
||||
if client.ApplicationType() == ApplicationTypeNative {
|
||||
return validateAuthReqRedirectURINative(client, uri)
|
||||
}
|
||||
if strings.HasPrefix(uri, "https://") {
|
||||
return checkURIAgainstRedirects(client, uri)
|
||||
}
|
||||
if client.ApplicationType() == ApplicationTypeNative {
|
||||
return validateAuthReqRedirectURINative(client, uri, responseType)
|
||||
}
|
||||
if err := checkURIAgainstRedirects(client, uri); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -327,14 +349,17 @@ 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
|
||||
func validateAuthReqRedirectURINative(client Client, uri string, responseType oidc.ResponseType) error {
|
||||
func validateAuthReqRedirectURINative(client Client, uri string) error {
|
||||
parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri)
|
||||
isCustomSchema := !strings.HasPrefix(uri, "http://")
|
||||
isCustomSchema := !(strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://"))
|
||||
if err := checkURIAgainstRedirects(client, uri); err == nil {
|
||||
if client.DevMode() {
|
||||
return nil
|
||||
}
|
||||
// The RedirectURIs are only valid for native clients when localhost or non-"http://"
|
||||
if !isLoopback && strings.HasPrefix(uri, "https://") {
|
||||
return nil
|
||||
}
|
||||
// The RedirectURIs are only valid for native clients when localhost or non-"http://" and "https://"
|
||||
if isLoopback || isCustomSchema {
|
||||
return nil
|
||||
}
|
||||
|
@ -359,16 +384,16 @@ func equalURI(url1, url2 *url.URL) bool {
|
|||
return url1.Path == url2.Path && url1.RawQuery == url2.RawQuery
|
||||
}
|
||||
|
||||
func HTTPLoopbackOrLocalhost(rawurl string) (*url.URL, bool) {
|
||||
parsedURL, err := url.Parse(rawurl)
|
||||
func HTTPLoopbackOrLocalhost(rawURL string) (*url.URL, bool) {
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
if parsedURL.Scheme != "http" {
|
||||
return nil, false
|
||||
if parsedURL.Scheme == "http" || parsedURL.Scheme == "https" {
|
||||
hostName := parsedURL.Hostname()
|
||||
return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback()
|
||||
}
|
||||
hostName := parsedURL.Hostname()
|
||||
return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ValidateAuthReqResponseType validates the passed response_type to the registered response types
|
||||
|
@ -406,6 +431,10 @@ func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r *
|
|||
|
||||
// AuthorizeCallback handles the callback after authentication in the Login UI
|
||||
func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||
ctx, span := tracer.Start(r.Context(), "AuthorizeCallback")
|
||||
r = r.WithContext(ctx)
|
||||
defer span.End()
|
||||
|
||||
id, err := ParseAuthorizeCallbackRequest(r)
|
||||
if err != nil {
|
||||
AuthRequestError(w, r, nil, err, authorizer)
|
||||
|
@ -438,6 +467,10 @@ func ParseAuthorizeCallbackRequest(r *http.Request) (id string, err error) {
|
|||
|
||||
// AuthResponse creates the successful authentication response (either code or tokens)
|
||||
func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := tracer.Start(r.Context(), "AuthResponse")
|
||||
r = r.WithContext(ctx)
|
||||
defer span.End()
|
||||
|
||||
client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID())
|
||||
if err != nil {
|
||||
AuthRequestError(w, r, authReq, err, authorizer)
|
||||
|
@ -450,26 +483,70 @@ func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWri
|
|||
AuthResponseToken(w, r, authReq, authorizer, client)
|
||||
}
|
||||
|
||||
// AuthResponseCode creates the successful code authentication response
|
||||
// AuthResponseCode handles the creation of a successful authentication response using an authorization code
|
||||
func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) {
|
||||
code, err := CreateAuthRequestCode(r.Context(), authReq, authorizer.Storage(), authorizer.Crypto())
|
||||
ctx, span := tracer.Start(r.Context(), "AuthResponseCode")
|
||||
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 {
|
||||
AuthRequestError(w, r, authReq, err, authorizer)
|
||||
return
|
||||
}
|
||||
codeResponse := struct {
|
||||
Code string `schema:"code"`
|
||||
State string `schema:"state,omitempty"`
|
||||
}{
|
||||
Code: code,
|
||||
State: authReq.GetState(),
|
||||
}
|
||||
callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder())
|
||||
}
|
||||
|
||||
// handleFormPostResponse processes the authentication response using form post method
|
||||
func handleFormPostResponse(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) error {
|
||||
codeResponse, err := BuildAuthResponseCodeResponsePayload(r.Context(), authReq, authorizer)
|
||||
if err != nil {
|
||||
AuthRequestError(w, r, authReq, err, authorizer)
|
||||
return
|
||||
return err
|
||||
}
|
||||
http.Redirect(w, r, callback, http.StatusFound)
|
||||
return AuthResponseFormPost(w, authReq.GetRedirectURI(), codeResponse, authorizer.Encoder())
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -484,6 +561,17 @@ func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthReque
|
|||
AuthRequestError(w, r, authReq, err, authorizer)
|
||||
return
|
||||
}
|
||||
|
||||
if authReq.GetResponseMode() == oidc.ResponseModeFormPost {
|
||||
err := AuthResponseFormPost(w, authReq.GetRedirectURI(), resp, authorizer.Encoder())
|
||||
if err != nil {
|
||||
AuthRequestError(w, r, authReq, err, authorizer)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), resp, authorizer.Encoder())
|
||||
if err != nil {
|
||||
AuthRequestError(w, r, authReq, err, authorizer)
|
||||
|
@ -494,6 +582,9 @@ func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthReque
|
|||
|
||||
// CreateAuthRequestCode creates and stores a code for the auth code response
|
||||
func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Storage, crypto Crypto) (string, error) {
|
||||
ctx, span := tracer.Start(ctx, "CreateAuthRequestCode")
|
||||
defer span.End()
|
||||
|
||||
code, err := BuildAuthRequestCode(authReq, crypto)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -535,6 +626,43 @@ func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, respons
|
|||
return mergeQueryParams(uri, params), nil
|
||||
}
|
||||
|
||||
//go:embed form_post.html.tmpl
|
||||
var formPostHtmlTemplate string
|
||||
|
||||
var formPostTmpl = template.Must(template.New("form_post").Parse(formPostHtmlTemplate))
|
||||
|
||||
// AuthResponseFormPost responds a html page that automatically submits the form which contains the auth response parameters
|
||||
func AuthResponseFormPost(res http.ResponseWriter, redirectURI string, response any, encoder httphelper.Encoder) error {
|
||||
values := make(map[string][]string)
|
||||
err := encoder.Encode(response, values)
|
||||
if err != nil {
|
||||
return oidc.ErrServerError().WithParent(err)
|
||||
}
|
||||
|
||||
params := &struct {
|
||||
RedirectURI string
|
||||
Params any
|
||||
}{
|
||||
RedirectURI: redirectURI,
|
||||
Params: values,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = formPostTmpl.Execute(&buf, params)
|
||||
if err != nil {
|
||||
return oidc.ErrServerError().WithParent(err)
|
||||
}
|
||||
|
||||
res.Header().Set("Cache-Control", "no-store")
|
||||
res.WriteHeader(http.StatusOK)
|
||||
_, err = buf.WriteTo(res)
|
||||
if err != nil {
|
||||
return oidc.ErrServerError().WithParent(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setFragment(uri *url.URL, params url.Values) string {
|
||||
uri.Fragment = params.Encode()
|
||||
return uri.String()
|
||||
|
|
|
@ -11,15 +11,15 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
|
||||
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/require"
|
||||
"github.com/zitadel/oidc/v3/example/server/storage"
|
||||
tu "github.com/zitadel/oidc/v3/internal/testutil"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"github.com/zitadel/oidc/v3/pkg/op/mock"
|
||||
"github.com/zitadel/schema"
|
||||
)
|
||||
|
||||
|
@ -137,11 +137,6 @@ func TestValidateAuthRequest(t *testing.T) {
|
|||
args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil},
|
||||
oidc.ErrInvalidRequest(),
|
||||
},
|
||||
{
|
||||
"scope openid missing fails",
|
||||
args{&oidc.AuthRequest{Scopes: []string{"profile"}}, mock.NewMockStorageExpectValidClientID(t), nil},
|
||||
oidc.ErrInvalidScope(),
|
||||
},
|
||||
{
|
||||
"response_type missing fails",
|
||||
args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil},
|
||||
|
@ -287,16 +282,6 @@ func TestValidateAuthReqScopes(t *testing.T) {
|
|||
err: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"scope openid missing fails",
|
||||
args{
|
||||
mock.NewClientExpectAny(t, op.ApplicationTypeWeb),
|
||||
[]string{"email"},
|
||||
},
|
||||
res{
|
||||
err: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"scope ok",
|
||||
args{
|
||||
|
@ -448,6 +433,24 @@ func TestValidateAuthReqRedirectURI(t *testing.T) {
|
|||
},
|
||||
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",
|
||||
args{
|
||||
|
@ -1027,9 +1030,10 @@ func TestAuthResponseCode(t *testing.T) {
|
|||
authorizer func(*testing.T) op.Authorizer
|
||||
}
|
||||
type res struct {
|
||||
wantCode int
|
||||
wantLocationHeader string
|
||||
wantBody string
|
||||
wantCode int
|
||||
wantLocationHeader string
|
||||
wantCacheControlHeader string
|
||||
wantBody string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -1071,7 +1075,7 @@ func TestAuthResponseCode(t *testing.T) {
|
|||
authorizer: func(t *testing.T) op.Authorizer {
|
||||
ctrl := gomock.NewController(t)
|
||||
storage := mock.NewMockStorage(ctrl)
|
||||
storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1")
|
||||
storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1")
|
||||
|
||||
authorizer := mock.NewMockAuthorizer(ctrl)
|
||||
authorizer.EXPECT().Storage().Return(storage)
|
||||
|
@ -1086,6 +1090,34 @@ func TestAuthResponseCode(t *testing.T) {
|
|||
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{
|
||||
|
@ -1096,7 +1128,7 @@ func TestAuthResponseCode(t *testing.T) {
|
|||
authorizer: func(t *testing.T) op.Authorizer {
|
||||
ctrl := gomock.NewController(t)
|
||||
storage := mock.NewMockStorage(ctrl)
|
||||
storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1")
|
||||
storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1")
|
||||
|
||||
authorizer := mock.NewMockAuthorizer(ctrl)
|
||||
authorizer.EXPECT().Storage().Return(storage)
|
||||
|
@ -1111,6 +1143,33 @@ func TestAuthResponseCode(t *testing.T) {
|
|||
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) {
|
||||
|
@ -1121,6 +1180,7 @@ func TestAuthResponseCode(t *testing.T) {
|
|||
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))
|
||||
|
@ -1165,6 +1225,133 @@ func Test_parseAuthorizeCallbackRequest(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -1195,3 +1382,231 @@ func TestValidateAuthReqIDTokenHint(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
"time"
|
||||
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
//go:generate go get github.com/dmarkham/enumer
|
||||
|
@ -92,6 +92,9 @@ type ClientJWTProfile interface {
|
|||
}
|
||||
|
||||
func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) {
|
||||
ctx, span := tracer.Start(ctx, "ClientJWTAuth")
|
||||
defer span.End()
|
||||
|
||||
if ca.ClientAssertion == "" {
|
||||
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
|
||||
}
|
||||
|
@ -104,6 +107,10 @@ func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier
|
|||
}
|
||||
|
||||
func ClientBasicAuth(r *http.Request, storage Storage) (clientID string, err error) {
|
||||
ctx, span := tracer.Start(r.Context(), "ClientBasicAuth")
|
||||
r = r.WithContext(ctx)
|
||||
defer span.End()
|
||||
|
||||
clientID, clientSecret, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
|
||||
|
@ -151,6 +158,10 @@ func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, au
|
|||
return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err)
|
||||
}
|
||||
|
||||
ctx, span := tracer.Start(r.Context(), "ClientIDFromRequest")
|
||||
r = r.WithContext(ctx)
|
||||
defer span.End()
|
||||
|
||||
data := new(clientData)
|
||||
if err = p.Decoder().Decode(data, r.Form); err != nil {
|
||||
return "", false, err
|
||||
|
@ -171,7 +182,7 @@ func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, au
|
|||
}
|
||||
// if the client did not send a Basic Auth Header, ignore the `ErrNoClientCredentials`
|
||||
// but return other errors immediately
|
||||
if err != nil && !errors.Is(err, ErrNoClientCredentials) {
|
||||
if !errors.Is(err, ErrNoClientCredentials) {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
|
|
|
@ -10,13 +10,13 @@ import (
|
|||
"strings"
|
||||
"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/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"github.com/zitadel/oidc/v3/pkg/op/mock"
|
||||
"github.com/zitadel/schema"
|
||||
)
|
||||
|
||||
|
@ -108,7 +108,7 @@ func TestClientBasicAuth(t *testing.T) {
|
|||
},
|
||||
storage: func() op.Storage {
|
||||
s := mock.NewMockStorage(gomock.NewController(t))
|
||||
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "wrong").Return(errWrong)
|
||||
s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "wrong").Return(errWrong)
|
||||
return s
|
||||
}(),
|
||||
wantErr: errWrong,
|
||||
|
@ -121,7 +121,7 @@ func TestClientBasicAuth(t *testing.T) {
|
|||
},
|
||||
storage: func() op.Storage {
|
||||
s := mock.NewMockStorage(gomock.NewController(t))
|
||||
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil)
|
||||
s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "bar").Return(nil)
|
||||
return s
|
||||
}(),
|
||||
wantClientID: "foo",
|
||||
|
@ -207,7 +207,7 @@ func TestClientIDFromRequest(t *testing.T) {
|
|||
p: testClientProvider{
|
||||
storage: func() op.Storage {
|
||||
s := mock.NewMockStorage(gomock.NewController(t))
|
||||
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil)
|
||||
s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "bar").Return(nil)
|
||||
return s
|
||||
}(),
|
||||
},
|
||||
|
|
|
@ -30,6 +30,7 @@ type Configuration interface {
|
|||
EndSessionEndpoint() *Endpoint
|
||||
KeysEndpoint() *Endpoint
|
||||
DeviceAuthorizationEndpoint() *Endpoint
|
||||
CheckSessionIframe() *Endpoint
|
||||
|
||||
AuthMethodPostSupported() bool
|
||||
CodeMethodS256Supported() bool
|
||||
|
@ -49,6 +50,9 @@ type Configuration interface {
|
|||
|
||||
SupportedUILocales() []language.Tag
|
||||
DeviceAuthorization() DeviceAuthorizationConfig
|
||||
|
||||
BackChannelLogoutSupported() bool
|
||||
BackChannelLogoutSessionSupported() bool
|
||||
}
|
||||
|
||||
type IssuerFromRequest func(r *http.Request) string
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package op
|
||||
|
||||
import (
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
|
||||
)
|
||||
|
||||
type Crypto interface {
|
||||
|
|
|
@ -9,12 +9,12 @@ import (
|
|||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
strs "github.com/zitadel/oidc/v3/pkg/strings"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
type DeviceAuthorizationConfig struct {
|
||||
|
@ -64,6 +64,10 @@ func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *htt
|
|||
}
|
||||
|
||||
func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error {
|
||||
ctx, span := tracer.Start(r.Context(), "DeviceAuthorization")
|
||||
r = r.WithContext(ctx)
|
||||
defer span.End()
|
||||
|
||||
req, err := ParseDeviceCodeRequest(r, o)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -78,16 +82,16 @@ func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvide
|
|||
}
|
||||
|
||||
func createDeviceAuthorization(ctx context.Context, req *oidc.DeviceAuthorizationRequest, clientID string, o OpenIDProvider) (*oidc.DeviceAuthorizationResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "createDeviceAuthorization")
|
||||
defer span.End()
|
||||
|
||||
storage, err := assertDeviceStorage(o.Storage())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config := o.DeviceAuthorization()
|
||||
|
||||
deviceCode, err := NewDeviceCode(RecommendedDeviceCodeBytes)
|
||||
if err != nil {
|
||||
return nil, NewStatusError(err, http.StatusInternalServerError)
|
||||
}
|
||||
deviceCode, _ := NewDeviceCode(RecommendedDeviceCodeBytes)
|
||||
userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval)
|
||||
if err != nil {
|
||||
return nil, NewStatusError(err, http.StatusInternalServerError)
|
||||
|
@ -127,6 +131,10 @@ func createDeviceAuthorization(ctx context.Context, req *oidc.DeviceAuthorizatio
|
|||
}
|
||||
|
||||
func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) {
|
||||
ctx, span := tracer.Start(r.Context(), "ParseDeviceCodeRequest")
|
||||
r = r.WithContext(ctx)
|
||||
defer span.End()
|
||||
|
||||
clientID, _, err := ClientIDFromRequest(r, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -152,11 +160,14 @@ func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuth
|
|||
// results in a 22 character base64 encoded string.
|
||||
const RecommendedDeviceCodeBytes = 16
|
||||
|
||||
// NewDeviceCode generates a new cryptographically secure device code as a base64 encoded string.
|
||||
// The length of the string is nBytes * 4 / 3.
|
||||
// An error is never returned.
|
||||
//
|
||||
// TODO(v4): change return type to string alone.
|
||||
func NewDeviceCode(nBytes int) (string, error) {
|
||||
bytes := make([]byte, nBytes)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("%w getting entropy for device code", err)
|
||||
}
|
||||
rand.Read(bytes)
|
||||
return base64.RawURLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
|
@ -265,7 +276,7 @@ func (r *DeviceAuthorizationState) GetAMR() []string {
|
|||
}
|
||||
|
||||
func (r *DeviceAuthorizationState) GetAudience() []string {
|
||||
if !strs.Contains(r.Audience, r.ClientID) {
|
||||
if !slices.Contains(r.Audience, r.ClientID) {
|
||||
r.Audience = append(r.Audience, r.ClientID)
|
||||
}
|
||||
return r.Audience
|
||||
|
@ -288,6 +299,9 @@ func (r *DeviceAuthorizationState) GetSubject() string {
|
|||
}
|
||||
|
||||
func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) {
|
||||
ctx, span := tracer.Start(ctx, "CheckDeviceAuthorizationState")
|
||||
defer span.End()
|
||||
|
||||
storage, err := assertDeviceStorage(exchanger.Storage())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -330,10 +344,11 @@ func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, c
|
|||
RefreshToken: refreshToken,
|
||||
TokenType: oidc.BearerToken,
|
||||
ExpiresIn: uint64(validity.Seconds()),
|
||||
Scope: tokenRequest.GetScopes(),
|
||||
}
|
||||
|
||||
// TODO(v4): remove type assertion
|
||||
if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && strs.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) {
|
||||
if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && slices.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) {
|
||||
response.IDToken, err = CreateIDToken(ctx, IssuerFromContext(ctx), idTokenRequest, client.IDTokenLifetime(), accessToken, "", creator.Storage(), client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -13,12 +13,12 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v3/example/server/storage"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
func Test_deviceAuthorizationHandler(t *testing.T) {
|
||||
|
@ -145,21 +145,11 @@ func runWithRandReader(r io.Reader, f func()) {
|
|||
}
|
||||
|
||||
func TestNewDeviceCode(t *testing.T) {
|
||||
t.Run("reader error", func(t *testing.T) {
|
||||
runWithRandReader(errReader{}, func() {
|
||||
_, err := op.NewDeviceCode(16)
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("different lengths, rand reader", func(t *testing.T) {
|
||||
for i := 1; i <= 32; i++ {
|
||||
got, err := op.NewDeviceCode(i)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i))
|
||||
}
|
||||
})
|
||||
|
||||
for i := 1; i <= 32; i++ {
|
||||
got, err := op.NewDeviceCode(i)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUserCode(t *testing.T) {
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
type DiscoverStorage interface {
|
||||
|
@ -45,6 +45,7 @@ func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage Di
|
|||
EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer),
|
||||
JwksURI: config.KeysEndpoint().Absolute(issuer),
|
||||
DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer),
|
||||
CheckSessionIframe: config.CheckSessionIframe().Absolute(issuer),
|
||||
ScopesSupported: Scopes(config),
|
||||
ResponseTypesSupported: ResponseTypes(config),
|
||||
GrantTypesSupported: GrantTypes(config),
|
||||
|
@ -61,6 +62,8 @@ func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage Di
|
|||
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
|
||||
UILocalesSupported: config.SupportedUILocales(),
|
||||
RequestParameterSupported: config.RequestObjectSupported(),
|
||||
BackChannelLogoutSupported: config.BackChannelLogoutSupported(),
|
||||
BackChannelLogoutSessionSupported: config.BackChannelLogoutSessionSupported(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,11 +95,17 @@ func createDiscoveryConfigV2(ctx context.Context, config Configuration, storage
|
|||
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
|
||||
UILocalesSupported: config.SupportedUILocales(),
|
||||
RequestParameterSupported: config.RequestObjectSupported(),
|
||||
BackChannelLogoutSupported: config.BackChannelLogoutSupported(),
|
||||
BackChannelLogoutSessionSupported: config.BackChannelLogoutSessionSupported(),
|
||||
}
|
||||
}
|
||||
|
||||
func Scopes(c Configuration) []string {
|
||||
return DefaultSupportedScopes // TODO: config
|
||||
provider, ok := c.(*Provider)
|
||||
if ok && provider.config.SupportedScopes != nil {
|
||||
return provider.config.SupportedScopes
|
||||
}
|
||||
return DefaultSupportedScopes
|
||||
}
|
||||
|
||||
func ResponseTypes(c Configuration) []string {
|
||||
|
@ -131,10 +140,13 @@ func GrantTypes(c Configuration) []oidc.GrantType {
|
|||
}
|
||||
|
||||
func SubjectTypes(c Configuration) []string {
|
||||
return []string{"public"} //TODO: config
|
||||
return []string{"public"} // TODO: config
|
||||
}
|
||||
|
||||
func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string {
|
||||
ctx, span := tracer.Start(ctx, "SigAlgorithms")
|
||||
defer span.End()
|
||||
|
||||
algorithms, err := storage.SignatureAlgorithms(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
|
|
|
@ -6,14 +6,14 @@ import (
|
|||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"github.com/zitadel/oidc/v3/pkg/op/mock"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestDiscover(t *testing.T) {
|
||||
|
@ -81,6 +81,11 @@ func Test_scopes(t *testing.T) {
|
|||
args{},
|
||||
op.DefaultSupportedScopes,
|
||||
},
|
||||
{
|
||||
"custom scopes",
|
||||
args{newTestProvider(&op.Config{SupportedScopes: []string{"test1", "test2"}})},
|
||||
[]string{"test1", "test2"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -3,8 +3,8 @@ package op_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
func TestEndpoint_Path(t *testing.T) {
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
type ErrAuthRequest interface {
|
||||
|
@ -46,6 +46,12 @@ func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthReq
|
|||
return
|
||||
}
|
||||
e.State = authReq.GetState()
|
||||
var sessionState string
|
||||
authRequestSessionState, ok := authReq.(AuthRequestSessionState)
|
||||
if ok {
|
||||
sessionState = authRequestSessionState.GetSessionState()
|
||||
}
|
||||
e.SessionState = sessionState
|
||||
var responseMode oidc.ResponseMode
|
||||
if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok {
|
||||
responseMode = rm.GetResponseMode()
|
||||
|
@ -92,6 +98,12 @@ func TryErrorRedirect(ctx context.Context, authReq ErrAuthRequest, parent error,
|
|||
}
|
||||
|
||||
e.State = authReq.GetState()
|
||||
var sessionState string
|
||||
authRequestSessionState, ok := authReq.(AuthRequestSessionState)
|
||||
if ok {
|
||||
sessionState = authRequestSessionState.GetSessionState()
|
||||
}
|
||||
e.SessionState = sessionState
|
||||
var responseMode oidc.ResponseMode
|
||||
if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok {
|
||||
responseMode = rm.GetResponseMode()
|
||||
|
|
|
@ -11,9 +11,9 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/schema"
|
||||
)
|
||||
|
||||
|
@ -428,7 +428,8 @@ func TestTryErrorRedirect(t *testing.T) {
|
|||
parent: oidc.ErrInteractionRequired().WithDescription("sign in"),
|
||||
},
|
||||
want: &Redirect{
|
||||
URL: "http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1",
|
||||
Header: make(http.Header),
|
||||
URL: "http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1",
|
||||
},
|
||||
wantLog: `{
|
||||
"level":"WARN",
|
||||
|
|
14
pkg/op/form_post.html.tmpl
Normal file
14
pkg/op/form_post.html.tmpl
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8" /></head>
|
||||
<body onload="javascript:document.forms[0].submit()">
|
||||
<form method="post" action="{{ .RedirectURI }}">
|
||||
{{with .Params.state}}<input type="hidden" name="state" value="{{ index . 0 }}"/>{{end}}
|
||||
{{with .Params.code}}<input type="hidden" name="code" value="{{ index . 0 }}" />{{end}}
|
||||
{{with .Params.id_token}}<input type="hidden" name="id_token" value="{{ index . 0 }}"/>{{end}}
|
||||
{{with .Params.access_token}}<input type="hidden" name="access_token" value="{{ index . 0 }}" />{{end}}
|
||||
{{with .Params.token_type}}<input type="hidden" name="token_type" value="{{ index . 0 }}" />{{end}}
|
||||
{{with .Params.expires_in}}<input type="hidden" name="expires_in" value="{{ index . 0 }}" />{{end}}
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
)
|
||||
|
||||
type KeyProvider interface {
|
||||
|
@ -20,6 +20,10 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
|
|||
}
|
||||
|
||||
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
|
||||
ctx, span := tracer.Start(r.Context(), "Keys")
|
||||
r = r.WithContext(ctx)
|
||||
defer span.End()
|
||||
|
||||
keySet, err := k.KeySet(r.Context())
|
||||
if err != nil {
|
||||
httphelper.MarshalJSONWithStatus(w, err, http.StatusInternalServerError)
|
||||
|
|
|
@ -7,13 +7,13 @@ import (
|
|||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"github.com/zitadel/oidc/v3/pkg/op/mock"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestKeys(t *testing.T) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: Authorizer)
|
||||
// Source: git.christmann.info/LARA/zitadel-oidc/v3/pkg/op (interfaces: Authorizer)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
@ -9,9 +9,9 @@ import (
|
|||
slog "log/slog"
|
||||
reflect "reflect"
|
||||
|
||||
http "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
|
||||
op "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
http "github.com/zitadel/oidc/v3/pkg/http"
|
||||
op "github.com/zitadel/oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
// MockAuthorizer is a mock of Authorizer interface.
|
||||
|
|
|
@ -4,12 +4,12 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v3"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/zitadel/schema"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
func NewAuthorizer(t *testing.T) op.Authorizer {
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
|
||||
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
func NewClient(t *testing.T) op.Client {
|
||||
|
|
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