Compare commits

...

202 commits
v3.3.0 ... main

Author SHA1 Message Date
653b807f5d replace github url 2025-06-20 09:45:28 +02:00
29d69ca2e0 add function to marshal aud into a string if the array has a len of 1, to comply with rfc 2025-06-20 09:39:40 +02:00
53c4d07b45 remove actions 2025-06-20 08:56:29 +02:00
154fbe6420 Revert "feat(op): always verify code challenge when available (#721)"
Some checks failed
Code scanning - action / CodeQL-Build (push) Failing after 2m48s
Release / Go 1.23 test (push) Has been cancelled
Release / Go 1.24 test (push) Has been cancelled
Release / release (push) Has been cancelled
Breaks OIDC for some not yet updated applications, that we use.

This reverts commit c51628ea27.
2025-06-20 08:44:27 +02:00
Fabienne Bühler
d6e37fa741
Merge pull request #758 from zitadel/hifabienne-patch-1
chore: update issue templates
2025-06-17 14:32:55 +02:00
Fabienne Bühler
8e1e5174fd
Delete .github/ISSUE_TEMPLATE/proposal.yaml 2025-06-17 11:17:14 +02:00
Fabienne Bühler
5618487a88
Update and rename improvement.yaml to enhancement.yaml 2025-06-17 11:16:34 +02:00
Fabienne Bühler
187878de63
update docs issue template, add type 2025-06-17 11:15:26 +02:00
Fabienne Bühler
e127c66db2
chore: update issue templates 2025-06-17 11:14:09 +02:00
dependabot[bot]
e1415ef2f3
chore(deps): bump golang.org/x/text from 0.25.0 to 0.26.0 (#755)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.25.0 to 0.26.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 09:50:55 +02:00
Livio Spring
f94bd541d7
feat: update end session request to pass all params according to specification (#754)
* feat: update end session request to pass all params according to specification

* register encoder
2025-06-05 13:19:51 +02:00
dependabot[bot]
7d57aaa999
chore(deps): bump codecov/codecov-action from 5.4.2 to 5.4.3 (#751)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.2 to 5.4.3.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.4.2...v5.4.3)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 15:22:02 +03:00
dependabot[bot]
668fb0d37a
chore(deps): bump golang.org/x/text from 0.24.0 to 0.25.0 (#742)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-08 08:04:53 +02:00
dependabot[bot]
4ed4d257ab
chore(deps): bump golang.org/x/oauth2 from 0.29.0 to 0.30.0 (#743)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-08 08:00:26 +02:00
Ayato
4f0ed79c0a
fix(op): Add mitigation for PKCE Downgrade Attack (#741)
* fix(op): Add mitigation for PKCE downgrade attack

* chore(op): add test for PKCE verification
2025-04-29 14:33:31 +00:00
Masahito Osako
5913c5a074
feat: enhance authentication response handling (#728)
- Introduced CodeResponseType struct to encapsulate response data.
- Added handleFormPostResponse and handleRedirectResponse functions to manage different response modes.
- Created BuildAuthResponseCodeResponsePayload and BuildAuthResponseCallbackURL functions for better modularity in response generation.
2025-04-29 14:17:28 +00:00
dependabot[bot]
b917cdc2e3
chore(deps): bump codecov/codecov-action from 5.4.0 to 5.4.2 (#737)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.0 to 5.4.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.4.0...v5.4.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 11:13:43 +02:00
dependabot[bot]
cb3ec3ac5f
chore(deps): bump golang.org/x/net from 0.36.0 to 0.38.0 (#739)
* chore(deps): bump golang.org/x/net from 0.36.0 to 0.38.0

Bumps [golang.org/x/net](https://github.com/golang/net) from 0.36.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.36.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* update runner to ubuntu 24.04

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Livio Spring <livio.a@gmail.com>
2025-04-22 11:05:39 +02:00
dependabot[bot]
7cc5fb6568
chore(deps): bump golang.org/x/text from 0.23.0 to 0.24.0 (#733)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 12:05:26 +00:00
dependabot[bot]
92972fd30f
chore(deps): bump golang.org/x/oauth2 from 0.28.0 to 0.29.0 (#734)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-09 15:03:06 +03:00
Ayato
c51628ea27
feat(op): always verify code challenge when available (#721)
Finally the RFC Best Current Practice for OAuth 2.0 Security has been approved.

According to the RFC:

> Authorization servers MUST support PKCE [RFC7636].
> 
> If a client sends a valid PKCE code_challenge parameter in the authorization request, the authorization server MUST enforce the correct usage of code_verifier at the token endpoint.

Isn’t it time we strengthen PKCE support a bit more?

This PR updates the logic so that PKCE is always verified, even when the Auth Method is not "none".
2025-03-24 18:00:04 +02:00
dependabot[bot]
7096406e71
chore(deps): bump github.com/zitadel/schema from 1.3.0 to 1.3.1 (#731)
Bumps [github.com/zitadel/schema](https://github.com/zitadel/schema) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/zitadel/schema/releases)
- [Changelog](https://github.com/zitadel/schema/blob/main/.releaserc.js)
- [Commits](https://github.com/zitadel/schema/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/zitadel/schema
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 12:19:20 +02:00
dependabot[bot]
c91db9e47b
chore(deps): bump github.com/zitadel/logging from 0.6.1 to 0.6.2 (#730)
Bumps [github.com/zitadel/logging](https://github.com/zitadel/logging) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/zitadel/logging/releases)
- [Changelog](https://github.com/zitadel/logging/blob/main/.releaserc.js)
- [Commits](https://github.com/zitadel/logging/compare/v0.6.1...v0.6.2)

---
updated-dependencies:
- dependency-name: github.com/zitadel/logging
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 12:11:07 +02:00
Iraq
f648c61cab
Merge pull request #729 from zitadel/update-go-version
chore: run 'go mod tidy'
2025-03-23 16:49:50 +00:00
Iraq Jaber
30acdaf63a chore: run 'go mod tidy' 2025-03-23 16:27:57 +00:00
dependabot[bot]
aeda5d7178
chore(deps): bump golang.org/x/text from 0.22.0 to 0.23.0 (#723)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 10:05:10 +00:00
dependabot[bot]
f3ee647005
chore(deps): bump golang.org/x/net from 0.33.0 to 0.36.0 (#727)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 12:02:56 +02:00
dependabot[bot]
c401ad6cb8
chore(deps): bump golang.org/x/oauth2 from 0.26.0 to 0.28.0 (#724)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.26.0 to 0.28.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.26.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 07:46:07 +01:00
Iraq
2c64de821d
chore: updating go to 1.24 (#726)
* chore: updating go to 1.24

* fixup! chore: updating go to 1.24

* fixup! fixup! chore: updating go to 1.24

* fix device test (drop read error)

* drop older go versions

* drop unrelated formatter changes

---------

Co-authored-by: Iraq Jaber <IraqJaber@gmail.com>
Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
2025-03-14 16:12:26 +01:00
Tim Möhlmann
efd6fdad7a
fix: ignore empty json strings for locale (#678)
* Revert "fix: ignore all unmarshal errors from locale (#673)"

This reverts commit fbf009fe75.

* fix: ignore empty json strings for locale
2025-03-14 10:30:08 +00:00
BitMasher
7a767d8568
feat: add CanGetPrivateClaimsFromRequest interface (#717) 2025-03-12 14:00:29 +02:00
dependabot[bot]
eb2f912c5e
chore(deps): bump codecov/codecov-action from 5.3.1 to 5.4.0 (#722)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.3.1...v5.4.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-07 16:37:54 +01:00
dependabot[bot]
6a80712fbe
chore(deps): bump github.com/go-jose/go-jose/v4 from 4.0.4 to 4.0.5 (#716)
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.4...v4.0.5)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-25 12:00:02 +02:00
minami yoshihiko
4ef9529012
feat: support for session_state (#712)
* add default signature algorithm

* implements session_state in auth_request.go

* add test

* Update pkg/op/auth_request.go

link to the standard

Co-authored-by: Tim Möhlmann <muhlemmer@gmail.com>

* add check_session_iframe

---------

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
Co-authored-by: Tim Möhlmann <muhlemmer@gmail.com>
2025-02-24 10:50:38 +00:00
Steve Ruckdashel
eb98343a65
fix: migrate deprecated io/ioutil.ReadFile to os.ReadFile (#714) 2025-02-21 09:52:02 +00:00
mqf20
add254f60c
docs(example): fixed creation of refresh token (#711)
Signed-off-by: mqf20 <mingqingfoo@gmail.com>
2025-02-19 14:44:34 +02:00
mqf20
b1e5aca629
docs(example): check and extend refresh token expiration (#698)
* extend refresh token expiration

* check refresh token expiration

* check refresh token expiration (fixed logic)

* formatting

---------

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
2025-02-13 11:48:04 +00:00
mqf20
c03a8c59ca
docs(example): check access token expiration (#702) 2025-02-13 11:34:29 +00:00
mqf20
37dd41e49b
docs(example): simplified deletion (#699)
* simplified deletion

* added docs
2025-02-13 11:26:00 +00:00
mqf20
03e5ff8345
docs(example): add auth time (#700) 2025-02-13 11:23:44 +00:00
dependabot[bot]
c3c1bd3a40
chore(deps): bump github.com/go-chi/chi/v5 from 5.2.0 to 5.2.1 (#706)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.2.0...v5.2.1)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 13:45:18 +02:00
dependabot[bot]
0d46df908e
chore(deps): bump golang.org/x/text from 0.21.0 to 0.22.0 (#708)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.21.0 to 0.22.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 10:11:18 +00:00
dependabot[bot]
4250aad1f7
chore(deps): bump golang.org/x/oauth2 from 0.25.0 to 0.26.0 (#707)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 12:08:45 +02:00
dependabot[bot]
8c9a536058
chore(deps): bump codecov/codecov-action from 5.1.2 to 5.3.1 (#703)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.1.2 to 5.3.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.1.2...v5.3.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-28 10:29:28 +02:00
dependabot[bot]
24c96c361d
chore(deps): bump github.com/bmatcuk/doublestar/v4 from 4.8.0 to 4.8.1 (#701)
Bumps [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/bmatcuk/doublestar/releases)
- [Commits](https://github.com/bmatcuk/doublestar/compare/v4.8.0...v4.8.1)

---
updated-dependencies:
- dependency-name: github.com/bmatcuk/doublestar/v4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-27 17:37:23 +02:00
Ramon
de2fd41f40
fix: allow native clients to use https:// on localhost redirects (#691) 2025-01-17 13:53:19 +00:00
dependabot[bot]
867a4806fd
chore(deps): bump github.com/bmatcuk/doublestar/v4 from 4.7.1 to 4.8.0 (#696)
Bumps [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) from 4.7.1 to 4.8.0.
- [Release notes](https://github.com/bmatcuk/doublestar/releases)
- [Commits](https://github.com/bmatcuk/doublestar/compare/v4.7.1...v4.8.0)

---
updated-dependencies:
- dependency-name: github.com/bmatcuk/doublestar/v4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-17 14:51:01 +01:00
dependabot[bot]
1f6a0d5d89
chore(deps): bump golang.org/x/oauth2 from 0.24.0 to 0.25.0 (#695)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-06 10:47:02 +02:00
Danila Fominykh
a0f67c0b4b
feat: add redirect URI-s ENV setting to web clients (#693)
Co-authored-by: FominykhDG <FominykhDG@cloudx.group>
2025-01-03 08:27:01 +00:00
Stefan Benz
8d971dcad8
chore: bump dependencies (#694) 2024-12-30 12:47:05 +02:00
dependabot[bot]
6c90652dfb
chore(deps): bump codecov/codecov-action from 5.1.1 to 5.1.2 (#692)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.1.1...v5.1.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-30 11:00:57 +02:00
dependabot[bot]
b36a8e2ec1
chore(deps): bump github.com/go-chi/chi/v5 from 5.1.0 to 5.2.0 (#689)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.1.0...v5.2.0)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-16 20:27:45 +02:00
dependabot[bot]
9a93b7c70d
chore(deps): bump golang.org/x/crypto from 0.25.0 to 0.31.0 (#688)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 11:33:24 +00:00
dependabot[bot]
cf6ce69d79
chore(deps): bump codecov/codecov-action from 5.0.7 to 5.1.1 (#687)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.7 to 5.1.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.0.7...v5.1.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 14:16:13 +01:00
dependabot[bot]
2513e21531
chore(deps): bump golang.org/x/text from 0.20.0 to 0.21.0 (#686)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 08:42:45 +01:00
dependabot[bot]
057601ff3f
chore(deps): bump codecov/codecov-action from 5.0.2 to 5.0.7 (#685)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.2 to 5.0.7.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.0.2...v5.0.7)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-26 11:41:27 +02:00
dependabot[bot]
67bd2f5720
chore(deps): bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#684)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-26 10:55:33 +02:00
dependabot[bot]
e2de68a7dd
chore(deps): bump github.com/jeremija/gosubmit from 0.2.7 to 0.2.8 (#683)
Bumps [github.com/jeremija/gosubmit](https://github.com/jeremija/gosubmit) from 0.2.7 to 0.2.8.
- [Commits](https://github.com/jeremija/gosubmit/compare/v0.2.7...v0.2.8)

---
updated-dependencies:
- dependency-name: github.com/jeremija/gosubmit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-26 10:04:43 +02:00
dependabot[bot]
a7833f828c
chore(deps): bump codecov/codecov-action from 4.6.0 to 5.0.2 (#682)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.6.0 to 5.0.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.6.0...v5.0.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 12:59:21 +02:00
isegura-eos-eng
6d20928028
refactor: mark pkg/strings as deprecated in favor of stdlib (#680)
* refactor: mark pkg/strings as deprecated in favor of stdlib

* format: reword deprecate notice and use doc links
2024-11-15 18:47:32 +02:00
Tim Möhlmann
1464268851
chore(deps): upgrade go to v1.23 (#681) 2024-11-15 07:26:03 +01:00
isegura-eos-eng
897c720070
fix(op): add scope to access token scope (#664) 2024-11-13 08:49:55 +00:00
Kevin Schoonover
8afb8b8d5f
feat(pkg/op): allow custom SupportedScopes (#675)
Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
2024-11-12 15:06:24 +00:00
dependabot[bot]
87ab011157
chore(deps): bump golang.org/x/oauth2 from 0.23.0 to 0.24.0 (#676)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 12:55:25 +02:00
dependabot[bot]
f194951e61
chore(deps): bump golang.org/x/text from 0.19.0 to 0.20.0 (#677)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 12:52:23 +02:00
David Sharnoff
fbf009fe75
fix: ignore all unmarshal errors from locale (#673) 2024-11-01 10:53:28 +02:00
Livio Spring
f1e4cb2245
feat(OP): add back channel logout support (#671)
* feat: add configuration support for back channel logout

* logout token

* indicate back channel logout support in discovery endpoint
2024-10-30 08:44:31 +00:00
lanseg
24869d2811
feat(example): Allow configuring some parameters with env variables (#663)
Co-authored-by: Andrey Rusakov <andrey.rusakov@camptocamp.com>
2024-10-21 20:59:28 +02:00
dependabot[bot]
9f7cbb0dbf
chore(deps): bump github.com/bmatcuk/doublestar/v4 from 4.6.1 to 4.7.1 (#666)
Bumps [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) from 4.6.1 to 4.7.1.
- [Release notes](https://github.com/bmatcuk/doublestar/releases)
- [Commits](https://github.com/bmatcuk/doublestar/compare/v4.6.1...v4.7.1)

---
updated-dependencies:
- dependency-name: github.com/bmatcuk/doublestar/v4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-15 14:12:28 +03:00
dependabot[bot]
5ae555e191
chore(deps): bump codecov/codecov-action from 4.5.0 to 4.6.0 (#662)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.5.0...v4.6.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-08 12:00:43 +03:00
dependabot[bot]
2abae36bd9
chore(deps): bump golang.org/x/text from 0.18.0 to 0.19.0 (#661)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.18.0 to 0.19.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 18:39:28 +03:00
cui fliter
97d7b28fc0
fix: fix slice init length (#658) 2024-10-04 14:56:57 +03:00
dependabot[bot]
61c3bb887b
chore(deps): bump github.com/zitadel/logging from 0.6.0 to 0.6.1 (#657)
Bumps [github.com/zitadel/logging](https://github.com/zitadel/logging) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/zitadel/logging/releases)
- [Changelog](https://github.com/zitadel/logging/blob/main/.releaserc.js)
- [Commits](https://github.com/zitadel/logging/compare/v0.6.0...v0.6.1)

---
updated-dependencies:
- dependency-name: github.com/zitadel/logging
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 12:46:21 +02:00
Ayato
3b64e792ed
feat(oidc): return defined error when discovery failed (#653)
* feat(oidc): return defined error when discovery failed

* Use errors.Join() to join errors

Co-authored-by: Tim Möhlmann <muhlemmer@gmail.com>

* Remove unnecessary field

Co-authored-by: Tim Möhlmann <muhlemmer@gmail.com>

* Fix order and message

Co-authored-by: Tim Möhlmann <muhlemmer@gmail.com>

* Fix error order

* Simplify error assertion

Co-authored-by: Tim Möhlmann <muhlemmer@gmail.com>

---------

Co-authored-by: Tim Möhlmann <muhlemmer@gmail.com>
2024-09-20 12:33:28 +03:00
Tim Möhlmann
b555396744
fix(oidc): set client ID to access token JWT (#650)
* fix(oidc): set client ID to access token JWT

* fix test
2024-09-10 11:50:54 +02:00
dependabot[bot]
98c1ab755d
chore(deps): bump golang.org/x/text from 0.17.0 to 0.18.0 (#648)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.17.0 to 0.18.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.17.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 13:49:22 +03:00
dependabot[bot]
6c28e8cb4b
chore(deps): bump golang.org/x/oauth2 from 0.22.0 to 0.23.0 (#647)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 13:31:08 +03:00
lanseg
e1633bdb93
feat: Define redirect uris with env variables (#644)
Co-authored-by: Andrey Rusakov <andrey.rusakov@camptocamp.com>
2024-09-03 08:13:06 +00:00
dependabot[bot]
5e464b4ed8
chore(deps): bump github.com/rs/cors from 1.11.0 to 1.11.1 (#645)
Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.11.0 to 1.11.1.
- [Commits](https://github.com/rs/cors/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: github.com/rs/cors
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-30 09:58:10 +03:00
dependabot[bot]
52e8b651d3
chore(deps): bump go.opentelemetry.io/otel from 1.28.0 to 1.29.0 (#643)
Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.28.0 to 1.29.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.28.0...v1.29.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
2024-08-26 08:13:38 +00:00
David Sharnoff
67688db4c1
fix: client assertions for Okta (#636)
* fix client assertions for Okta

* review feedback
2024-08-26 11:11:01 +03:00
Tim Möhlmann
1e75773eaa
fix(op): initialize http Headers in response objects (#637)
* fix(op): initialize http Headers in response objects

* fix test

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-08-21 09:34:26 +02:00
Tim Möhlmann
99301930ed
feat(crypto): hash algorithm for EdDSA (#638)
* feat(crypto): hash algorithm for EdDSA

* update code comment

* rp: modify keytype check to support EdDSA

* example: signing algs from discovery

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-08-21 07:32:13 +00:00
Tim Möhlmann
0aa61b0b98
fix(op): do not redirect to unverified uri on error (#640)
Closes #627
2024-08-21 09:29:14 +02:00
dependabot[bot]
de034c8d24
chore(deps): bump golang.org/x/text from 0.16.0 to 0.17.0 (#633)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.16.0 to 0.17.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 09:52:23 +00:00
Tim Möhlmann
b6f3b1e65b
feat(op): allow returning of parent errors to client (#629)
* feat(op): allow returning of parent errors to client

* update godoc

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-08-09 05:10:11 +00:00
Elio Bischof
6f0a630ad4
fix: overwrite redirect content length (#632)
* fix: overwrite redirect content length

* copy redirect struct headers
2024-08-06 12:58:52 +03:00
dependabot[bot]
8f80225a20
chore(deps): bump golang.org/x/oauth2 from 0.21.0 to 0.22.0 (#631)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.21.0 to 0.22.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-06 12:07:00 +03:00
dependabot[bot]
b9bcd6aef9
chore(deps): bump github.com/go-jose/go-jose/v4 from 4.0.3 to 4.0.4 (#625)
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.3 to 4.0.4.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.3...v4.0.4)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-29 11:14:03 +03:00
dependabot[bot]
7b8be4387a
chore(deps): bump github.com/go-jose/go-jose/v4 from 4.0.2 to 4.0.3 (#624)
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.2...v4.0.3)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-10 13:37:53 +02:00
Livio Spring
e5a428d4be
feat: support PKCS#8 (#623) 2024-07-09 15:55:50 +02:00
dependabot[bot]
fc6716bf22
chore(deps): bump go.opentelemetry.io/otel from 1.27.0 to 1.28.0 (#622)
Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.27.0 to 1.28.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.27.0...v1.28.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-04 08:32:12 +02:00
dependabot[bot]
d6b4dc6b2f
chore(deps): bump actions/add-to-project from 1.0.1 to 1.0.2 (#620)
Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/actions/add-to-project/releases)
- [Commits](https://github.com/actions/add-to-project/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: actions/add-to-project
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-02 09:20:44 +02:00
dependabot[bot]
e87f433e09
chore(deps): bump github.com/go-chi/chi/v5 from 5.0.14 to 5.1.0 (#619)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.14 to 5.1.0.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.14...v5.1.0)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 12:20:28 +03:00
dkaminer
954802b63b
Updating indirect dependencies version in the OIDC GitHub library (#618)
golang.org/x/crypto, Version: v0.22.0 -→ v0.24.0
golang.org/x/net, Version: v0.23.0 -→ v0.26.0
golang.org/x/sys, Version: v0.19.0 -→ v0.21.0

Co-authored-by: Daphna Kaminer <daphna.kaminer@crowdstrike.com>
2024-06-27 09:05:47 +00:00
dependabot[bot]
a09d9f7390
chore(deps): bump github.com/go-chi/chi/v5 from 5.0.13 to 5.0.14 (#617)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.13 to 5.0.14.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.13...v5.0.14)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 10:42:22 +02:00
dependabot[bot]
371a5aaab4
chore(deps): bump github.com/go-chi/chi/v5 from 5.0.12 to 5.0.13 (#616)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.12 to 5.0.13.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.12...v5.0.13)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-22 10:08:01 +02:00
dependabot[bot]
1c2dc2c0e1
chore(deps): bump codecov/codecov-action from 4.4.1 to 4.5.0 (#615)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.4.1 to 4.5.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.4.1...v4.5.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-18 07:31:39 +02:00
Tim Möhlmann
da4e683bd3
fix(example): set content-type in the userinfo response (#614)
This change sets the `content-type` header to `application/json` for the response sent to the browser in the app example.
This enables pretty-printing of the userinfo json document in at least Chromium.
2024-06-14 07:40:05 +02:00
Tim Möhlmann
a7b5355580
feat(op): allow scope without openid (#613)
This changes removes the requirement of the openid scope to be set for all token requests.
As this library also support OAuth2-only authentication mechanisms we still want to sanitize requested scopes, but not enforce the openid scope.

Related to https://github.com/zitadel/zitadel/discussions/8068
2024-06-13 08:16:46 +02:00
dependabot[bot]
9ecdd0cf9a
chore(deps): bump golang.org/x/oauth2 from 0.20.0 to 0.21.0 (#611)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.20.0 to 0.21.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-07 08:16:06 +00:00
dependabot[bot]
7a8f8ade4d
chore(deps): bump golang.org/x/text from 0.15.0 to 0.16.0 (#612)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.15.0 to 0.16.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-07 10:14:04 +02:00
dependabot[bot]
7037344cf4
--- (#610)
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 10:23:36 +02:00
dependabot[bot]
7714a3b113
--- (#609)
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-21 12:56:32 +02:00
minami yoshihiko
8a47532a8e
feat: add default signature algorithms (#606) 2024-05-17 10:17:54 +00:00
dependabot[bot]
7437309a42
chore(deps): bump github.com/go-jose/go-jose/v4 from 4.0.1 to 4.0.2 (#608)
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.1 to 4.0.2.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.1...v4.0.2)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-17 10:56:21 +02:00
dependabot[bot]
6d1231cb37
chore(deps): bump codecov/codecov-action from 4.3.0 to 4.3.1 (#604)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.3.0...v4.3.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 07:58:56 +02:00
dependabot[bot]
20d0f189a8
chore(deps): bump golang.org/x/oauth2 from 0.19.0 to 0.20.0 (#601)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 06:44:21 +00:00
dependabot[bot]
30184ae054
chore(deps): bump golang.org/x/text from 0.14.0 to 0.15.0 (#600)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.14.0 to 0.15.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-05-06 06:41:53 +00:00
Yuval Marcus
5a84d8c4bc
fix: Omit non-standard, empty fields in RefreshTokenRequest when performing a token refresh (#599)
* Add omitempty tags

* Add omitempty to more fields
2024-05-06 08:13:52 +02:00
Yuval Marcus
24d43f538e
fix: Handle case where verifier Nonce func is nil (#594)
* Skip nonce check if verifier nonce func is nil

* add unit test
2024-05-02 09:46:12 +02:00
Tim Möhlmann
37ca0e472a
feat(op): authorize callback handler as argument in legacy server registration (#598)
This change requires an additional argument to the op.RegisterLegacyServer constructor which passes the Authorize Callback Handler.
This allows implementations to use their own handler instead of the one provided by the package.
The current handler is exported for legacy behavior.

This change is not considered breaking, as RegisterLegacyServer is flagged experimental.

Related to https://github.com/zitadel/zitadel/issues/6882
2024-04-30 20:27:12 +03:00
dependabot[bot]
099081fc1e
chore(deps): bump github.com/rs/cors from 1.10.1 to 1.11.0 (#596)
Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.10.1 to 1.11.0.
- [Commits](https://github.com/rs/cors/compare/v1.10.1...v1.11.0)

---
updated-dependencies:
- dependency-name: github.com/rs/cors
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-26 08:17:21 +00:00
dependabot[bot]
3e329dd049
chore(deps): bump go.opentelemetry.io/otel from 1.25.0 to 1.26.0 (#595)
Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.25.0 to 1.26.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.25.0...v1.26.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-26 10:15:22 +02:00
Kotaro Otaka
3512c72f1c
fix: to propagate context (#593)
Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-04-22 11:40:21 +00:00
dependabot[bot]
79daaf1a7a
chore(deps): bump golang.org/x/net from 0.22.0 to 0.23.0 (#592)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 20:27:12 +03:00
Kotaro Otaka
68d4e08f6d
feat: Added the ability to verify ID tokens using the value of id_token_signing_alg_values_supported retrieved from DiscoveryEndpoint (#579)
* feat(rp): to use signing algorithms from discovery configuration (#574)

* feat: WithSigningAlgsFromDiscovery to verify IDTokenVerifier() behavior in RP with
2024-04-16 08:41:31 +00:00
Ethan Heilman
959376bde7
Fixes typos in GoDoc and comments (#591) 2024-04-16 08:18:32 +00:00
dependabot[bot]
a77d773ca3
chore(deps): bump codecov/codecov-action from 4.2.0 to 4.3.0 (#590)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 08:16:57 +00:00
dependabot[bot]
3fa4891f3e
chore(deps): bump actions/add-to-project from 1.0.0 to 1.0.1 (#589)
Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/actions/add-to-project/releases)
- [Commits](https://github.com/actions/add-to-project/compare/v1.0.0...v1.0.1)

---
updated-dependencies:
- dependency-name: actions/add-to-project
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 11:15:39 +03:00
Tim Möhlmann
33f8df7eb2
feat(deps): update go-jose to v4 (#588)
This change updates to go-jose v4, which was a new major release.

jose.ParseSigned now expects the supported signing algorithms to be passed, on which we previously did our own check. As they use a dedicated type for this, the slice of string needs to be converted. The returned error also need to be handled in a non-standard way in order to stay compatible.

For OIDC v4 we should use the jose.SignatureAlgorithm  type directly and wrap errors, instead of returned static defined errors.

Closes #583
2024-04-11 18:13:30 +03:00
Jan-Otto Kröpke
06f37f84c1
fix: Fail safe, if optional endpoints are not given (#582) 2024-04-09 13:02:31 +00:00
dependabot[bot]
8a21d38136
chore(deps): bump codecov/codecov-action from 4.1.1 to 4.2.0 (#585)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.1.1 to 4.2.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.1.1...v4.2.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 12:39:36 +03:00
Célian GARCIA
e75a061807
feat: support verification_url workaround for DeviceAuthorizationResponse unmarshal (#577) 2024-04-08 13:43:31 +00:00
dependabot[bot]
33485b82ba
chore(deps): bump go.opentelemetry.io/otel from 1.24.0 to 1.25.0 (#584)
Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.24.0 to 1.25.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.24.0...v1.25.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 10:57:09 +03:00
dependabot[bot]
370738772a
chore(deps): bump golang.org/x/oauth2 from 0.18.0 to 0.19.0 (#580)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 10:52:08 +03:00
Livio Spring
a3b73a6950
chore(workflow): fix action/add-to-project version (#578) 2024-04-03 19:32:50 +03:00
dependabot[bot]
5cdb65c30b
chore(deps): bump actions/add-to-project from 0.6.1 to 1.0.0 (#575)
* chore(deps): bump actions/add-to-project from 0.6.1 to 1.0.0

Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 0.6.1 to 1.0.0.
- [Release notes](https://github.com/actions/add-to-project/releases)
- [Commits](https://github.com/actions/add-to-project/compare/v0.6.1...v1.0.0)

---
updated-dependencies:
- dependency-name: actions/add-to-project
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update issue.yml

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-04-02 06:22:36 +00:00
dependabot[bot]
d729c22526
chore(deps): bump codecov/codecov-action from 4.1.0 to 4.1.1 (#576)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 07:58:28 +02:00
Célian GARCIA
c89d0ed970
feat: return oidc.Error in case of call token failure (#571) 2024-04-01 13:55:22 +00:00
dependabot[bot]
910f55ea7b
chore(deps): bump actions/add-to-project from 0.6.0 to 0.6.1 (#572)
Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/actions/add-to-project/releases)
- [Commits](https://github.com/actions/add-to-project/compare/v0.6.0...v0.6.1)

---
updated-dependencies:
- dependency-name: actions/add-to-project
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-26 07:15:38 +01:00
Tim Möhlmann
56397f88d5
feat(oidc): add actor claim to introspection response (#570)
With impersonation we assign an actor claim to our JWT/ID Tokens. This change adds the actor claim to the introspection response to follow suit.

This PR also adds the `auth_time` and `amr` claims for consistency.
2024-03-18 11:36:16 +01:00
Tim Möhlmann
4d63d68c9e
feat(op): allow setting the actor to Token Requests (#569)
For impersonation token exchange we need to persist the actor throughout token requests, including refresh token.
This PR adds the optional TokenActorRequest interface which allows to pass such actor.
2024-03-14 06:57:44 +00:00
dependabot[bot]
9afc07c0cb
chore(deps): bump google.golang.org/protobuf from 1.31.0 to 1.33.0 (#568)
Bumps google.golang.org/protobuf from 1.31.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-14 06:55:56 +00:00
Silvan
aae9c86f1a
Merge pull request #564 from zitadel/extend-tracing
feat(op): extend tracing for more detailed spans
feat(client): add possibility of tracing client calls
2024-03-14 07:53:57 +01:00
adlerhurst
565a022e91 Merge branch 'extend-tracing' of https://github.com/zitadel/oidc into extend-tracing 2024-03-14 07:51:35 +01:00
adlerhurst
03f3bc693b fix test 2024-03-14 07:50:29 +01:00
Silvan
0ffd13c780
Merge branch 'main' into extend-tracing 2024-03-13 15:45:19 +01:00
adlerhurst
1b94f796eb move tracer to client,
add tracing in rs, client
2024-03-13 15:45:03 +01:00
Tim Möhlmann
ad79802968
feat: extend token exchange response (#567)
* feat: extend token exchange response

This change adds fields to the token exchange and token claims types.

The `act` claim has been added to describe the actor in case of impersonation or delegation. 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).

The `id_token` field has been added to the Token Exchange response  so an ID Token can be returned along with an access token. This is not specified in RFC 8693, but it allows us be consistent with OpenID responses when the scope `openid` is set, while the requested token type may remain access token.

* allow jwt profile for token exchange client

* add invalid target error
2024-03-13 16:26:09 +02:00
dependabot[bot]
1532a5c78b
chore(deps): bump github.com/go-jose/go-jose/v3 from 3.0.2 to 3.0.3 (#566)
Bumps [github.com/go-jose/go-jose/v3](https://github.com/go-jose/go-jose) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/v3.0.3/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v3.0.2...v3.0.3)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-08 07:35:47 +01:00
adlerhurst
0fe7c3307f fix parse 2024-03-07 15:25:23 +01:00
adlerhurst
7069813ec7 correct span names 2024-03-07 10:44:24 +01:00
adlerhurst
88209ac11d fix tests 2024-03-06 19:08:48 +01:00
adlerhurst
bdcccc3303 feat(client): tracing in rp 2024-03-06 18:39:27 +01:00
adlerhurst
d18aba8cb3 feat(rp): extend tracing 2024-03-06 18:38:37 +01:00
Tim Möhlmann
e3e48882df
chore: upgrade to v3 guide (#463)
* chore: upgrade to v3 guide

first version with sed scripts.

* tidy up introduction info

* process feedback from @muir

* logging chapter

* server interface chapter

* update readme with v3 badges and link to update guide

* resolve comments
2024-03-05 13:09:14 +00:00
Ayato
5ef597b1db
feat(op): Add response_mode: form_post (#551)
* feat(op): Add response_mode: form_post

* Fix to parse the template ahead of time

* Fix to render the template in a buffer

* Remove unnecessary import

* Fix test

* Fix example client setting

* Make sure the client not to reuse the content of the response

* Fix error handling

* Add the response_mode param

* Allow implicit flow in the example app

* feat(rp): allow form_post in code exchange callback handler

---------

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
2024-03-05 15:04:43 +02:00
dependabot[bot]
fc743a69c7
chore(deps): bump golang.org/x/oauth2 from 0.17.0 to 0.18.0 (#562)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.17.0 to 0.18.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.17.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-05 11:07:48 +01:00
dependabot[bot]
7bac3c6f40
chore(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#560)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 08:10:55 +01:00
Tim Möhlmann
972b8981e5
feat: go 1.22 and slog migration (#557)
This change adds Go 1.22 as a build target and drops support for Go 1.20 and older. The golang.org/x/exp/slog import is migrated to log/slog.

Slog has been part of the Go standard library since Go 1.21. Therefore we are dropping support for older Go versions. This is in line of our support policy of "the latest two Go versions".
2024-02-28 10:44:14 +01:00
dependabot[bot]
38c025f7f8
chore(deps): bump codecov/codecov-action from 4.0.1 to 4.1.0 (#559)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.0.1 to 4.1.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.0.1...v4.1.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 10:09:14 +02:00
dependabot[bot]
385060930d
chore(deps): bump actions/add-to-project from 0.5.0 to 0.6.0 (#558)
Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/actions/add-to-project/releases)
- [Commits](https://github.com/actions/add-to-project/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: actions/add-to-project
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 10:05:38 +02:00
dependabot[bot]
b93f625088
chore(deps): bump github.com/go-jose/go-jose/v3 from 3.0.1 to 3.0.2 (#554)
Bumps [github.com/go-jose/go-jose/v3](https://github.com/go-jose/go-jose) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v3.0.1...v3.0.2)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 10:47:11 +02:00
dependabot[bot]
a6a206b021
chore(deps): bump go.opentelemetry.io/otel/trace from 1.23.1 to 1.24.0 (#556)
Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.23.1 to 1.24.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.23.1...v1.24.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/trace
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 10:45:58 +02:00
Jan-Otto Kröpke
f4bbffb51b
feat: Add rp.WithAuthStyle as Option (#546)
* feat: Add rp.WithAuthStyle as Option

* Update integration_test.go

* Update integration_test.go

* Update integration_test.go
2024-02-23 12:18:06 +02:00
Jan-Otto Kröpke
b45072a4c0
fix: Set unauthorizedHandler, if not defined (#547) 2024-02-21 12:17:00 +02:00
dependabot[bot]
3e593474e9
chore(deps): bump github.com/go-chi/chi/v5 from 5.0.11 to 5.0.12 (#548)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.11 to 5.0.12.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.11...v5.0.12)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 12:14:41 +02:00
Fabi
c5619ab4ff
Merge pull request #544 from zitadel/livio-a-patch-1
chore: ignore dependabot for board PRs
2024-02-13 10:53:35 +01:00
dependabot[bot]
da8b73f342
chore(deps): bump golang.org/x/oauth2 from 0.16.0 to 0.17.0 (#542)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-09 15:16:10 +00:00
dependabot[bot]
1eebaf8d6f
chore(deps): bump go.opentelemetry.io/otel from 1.23.0 to 1.23.1 (#540)
Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.23.0 to 1.23.1.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.23.0...v1.23.1)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-09 16:14:46 +01:00
dependabot[bot]
625a4e480d
chore(deps): bump go.opentelemetry.io/otel/trace from 1.23.0 to 1.23.1 (#539)
Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.23.0 to 1.23.1.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.23.0...v1.23.1)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/trace
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-09 16:14:24 +01:00
Livio Spring
ee8152f19e
chore: ignore dependabot for board PRs 2024-02-09 16:11:59 +01:00
dependabot[bot]
3ea6173860
chore(deps): bump actions-ecosystem/action-add-labels (#530)
Bumps [actions-ecosystem/action-add-labels](https://github.com/actions-ecosystem/action-add-labels) from 1.1.0 to 1.1.3.
- [Release notes](https://github.com/actions-ecosystem/action-add-labels/releases)
- [Commits](https://github.com/actions-ecosystem/action-add-labels/compare/v1.1.0...v1.1.3)

---
updated-dependencies:
- dependency-name: actions-ecosystem/action-add-labels
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 18:30:44 +02:00
dependabot[bot]
34f44325b8
chore(deps): bump go.opentelemetry.io/otel/trace from 1.22.0 to 1.23.0 (#534)
Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.22.0 to 1.23.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.22.0...v1.23.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/trace
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 18:29:38 +02:00
dependabot[bot]
7a45a86452
chore(deps): bump codecov/codecov-action from 3.1.5 to 4.0.1 (#531)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.5 to 4.0.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3.1.5...v4.0.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 18:26:46 +02:00
Fabi
0992c5f3ce
Merge pull request #536 from zitadel/livio-a-patch-1
chore: ignore dependabot for board PRs
2024-02-07 07:33:07 +01:00
Livio Spring
25e103b243
chore: ignore dependabot for board PRs 2024-02-07 07:30:04 +01:00
Fabi
984346f9ef
chore: remove dependabot prs (#529) 2024-02-02 14:33:52 +01:00
Fabi
2aa8a327f6
chore: update pm board action (#528)
* chore: update pm board action 

automatically ad prs of non engineers to board and label community prs

* Update issue.yml
2024-02-02 12:57:41 +02:00
Tim Möhlmann
045b59e5a5
fix(op): allow expired id token hints in authorize (#527)
Like https://github.com/zitadel/oidc/pull/522 for end session,
this change allows passing an expired ID token hint to the authorize endpoint.
2024-02-01 13:49:22 +01:00
dependabot[bot]
35d9540fd7
chore(deps): bump codecov/codecov-action from 3.1.4 to 3.1.5 (#526)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3.1.4...v3.1.5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-30 11:56:16 +02:00
Tim Möhlmann
e9bd7d7bac
feat(op): split the access and ID token hint verifiers (#525)
* feat(op): split the access and ID token hint verifiers

In zitadel we require different behaviors wrt public key expiry between access tokens and ID token hints.
This change splits the two verifiers in the OP.
The default is still based on Storage and passed to both verifier fields.

* add new options to tests
2024-01-26 16:44:50 +01:00
dependabot[bot]
437a0497ab
chore(deps): bump github.com/google/uuid from 1.5.0 to 1.6.0 (#523)
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.5.0...v1.6.0)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-24 13:54:30 +02:00
Tim Möhlmann
b8e520afd0
fix: allow expired ID token hint to end sessions (#522)
* fix: allow expired ID token hint to end sessions

This change adds a specific error for expired ID Token hints, including too old "issued at" and "max auth age".
The error is returned VerifyIDTokenHint so that the end session handler can choose to ignore this error.

This fixes the behavior to be in line with [OpenID Connect RP-Initiated Logout 1.0, section 4](https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ValidationAndErrorHandling).

* Tes IDTokenHintExpiredError
2024-01-19 11:30:51 +01:00
dependabot[bot]
3f26eb10ad
chore(deps): bump go.opentelemetry.io/otel/trace from 1.21.0 to 1.22.0 (#520)
Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.21.0 to 1.22.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.21.0...v1.22.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/trace
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 12:26:18 +02:00
Tim Möhlmann
57d04e7465
fix: don't force server errors in legacy server (#517)
* fix: don't force server errors in legacy server

* fix tests and be more consistent with the returned status code
2024-01-17 16:06:45 +01:00
Tim Möhlmann
844e2337bb
fix(op): check redirect URI in code exchange (#516)
This changes fixes a missing redirect check in the Legacy Server's Code Exchange handler.
2024-01-16 07:18:41 +01:00
Jan-Otto Kröpke
984e31a9e2
feat(rp): Add UnauthorizedHandler (#503)
* RP: Add UnauthorizedHandler

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>

* remove race condition

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>

* Use optional interface

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>

---------

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
2024-01-09 17:24:05 +02:00
dependabot[bot]
5dcf6de055
chore(deps): bump golang.org/x/oauth2 from 0.15.0 to 0.16.0 (#513)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.15.0 to 0.16.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 17:17:04 +02:00
Tim Möhlmann
4d85375702
chore(example): add device package level documentation (#510) 2024-01-08 10:21:28 +01:00
Tim Möhlmann
8923b82142
chore(deps): enable dependabot for the v2 branch (#512) 2024-01-08 10:18:33 +01:00
Jan-Otto Kröpke
e23b1d4754
fix: Implement dedicated error for RevokeToken (#508)
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
2024-01-08 10:01:34 +02:00
Tim Möhlmann
c37ca25220
feat(op): allow double star globs (#507)
Related to https://github.com/zitadel/zitadel/issues/5110
2024-01-05 17:30:17 +02:00
Tim Möhlmann
dce79a73fb
fix(oidc): ignore unknown language tag in userinfo unmarshal (#505)
* fix(oidc): ignore unknown language tag in userinfo unmarshal

Open system reported an issue where a generic OpenID provider might return language tags like "gb".
These tags are well-formed but unknown and Go returns an error for it.
We already ignored unknown tags is ui_locale arrays lik in AuthRequest.

This change ignores singular unknown tags, like used in the userinfo `locale` claim.

* do not set nil to Locale field
2023-12-22 10:25:58 +01:00
dependabot[bot]
6a8e144e8d
chore(deps): bump github.com/go-chi/chi/v5 from 5.0.10 to 5.0.11 (#504)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.10 to 5.0.11.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.0.10...v5.0.11)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-20 18:26:55 +02:00
dependabot[bot]
2b35eeb835
chore(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#502)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 12:15:36 +02:00
dependabot[bot]
e6d41bdd5d
chore(deps): bump github/codeql-action from 2 to 3 (#501)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 11:54:35 +02:00
Tim Möhlmann
b300027cd7
feat(op): ID token for device authorization grant (#500) 2023-12-18 08:39:39 +01:00
snow
7bdaf9c71d
feat(op): User-configurable claims_supported (#495)
* User-configurable claims_supported

* Use op.SupportedClaims instead of interface
2023-12-17 12:06:42 +00:00
dependabot[bot]
bca8833c15
chore(deps): bump github.com/google/uuid from 1.4.0 to 1.5.0 (#499)
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.4.0...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-14 11:59:11 +02:00
dependabot[bot]
9c582989d9
chore(deps): bump actions/setup-go from 4 to 5 (#498)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-14 11:58:03 +02:00
Stephen Andary
9d12d1d900
feat(op): PKCE Verification in Legacy Server when AuthMethod is not NONE and CodeVerifier is not Empty (#496)
* add logic for legacy server pkce verification when auth method is not None, and code verifier is not empty.

* update per Tim's direction
2023-12-07 17:36:03 +02:00
mffap
ed21cdd4ce
docs: update features client credential grant (#497)
Introduced with https://github.com/zitadel/oidc/pull/494
2023-12-06 11:51:24 +02:00
Oleksandr Shepetko
3a4d44cae7
fix(crypto): nil pointer dereference in crypto.BytesToPrivateKey (#491) (#493) 2023-12-05 17:15:59 +02:00
Tim Möhlmann
fe3e02b80a
feat(rp): client credentials grant (#494)
This change adds Client Credentials grant to the Relying Party.
As specified in [RFC 6749, section 4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)
2023-12-05 06:40:16 +01:00
dependabot[bot]
4d05eade5e
chore(deps): bump golang.org/x/oauth2 from 0.14.0 to 0.15.0 (#492)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.14.0 to 0.15.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 09:59:39 +02:00
Stefan Benz
a8ef8de87b
feat(op): JWT profile verifier with keyset
feat(op): JWT profile verifier with keyset
2023-11-21 10:26:57 +01:00
Jan-Otto Kröpke
7d0cdec925
fix(examples): Offer Storage with non-global client (#489) 2023-11-20 14:40:42 +02:00
Tim Möhlmann
f6bd17e8db correct comment 2023-11-13 19:28:01 +02:00
Tim Möhlmann
c6b5544516 Merge branch 'main' into perf-introspection 2023-11-13 18:17:09 +02:00
Tim Möhlmann
f7a0f7cb0b feat(op): create a JWT profile with a keyset 2023-11-10 09:36:08 +02:00
145 changed files with 4127 additions and 1110 deletions

View file

@ -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." 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]: " title: "[Bug]: "
labels: ["bug"] labels: ["bug"]
type: Bug
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -1,6 +1,7 @@
name: 📄 Documentation name: 📄 Documentation
description: Create an issue for missing or wrong documentation. description: Create an issue for missing or wrong documentation.
labels: ["docs"] labels: ["docs"]
type: task
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -1,11 +1,12 @@
name: 🛠️ Improvement name: 🛠️ Improvement
description: "Create an new issue for an improvment in ZITADEL" description: "Create an new issue for an improvment in ZITADEL"
labels: ["improvement"] labels: ["enhancement"]
type: enhancement
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: checkboxes
id: preflight id: preflight
attributes: attributes:

View file

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

View file

@ -29,7 +29,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
# Override language selection by uncommenting this and choosing your languages # Override language selection by uncommenting this and choosing your languages
with: with:
languages: go languages: go
@ -37,7 +37,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -51,4 +51,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View file

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

View file

@ -14,25 +14,25 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
go: ['1.19', '1.20', '1.21'] go: ['1.23', '1.24']
name: Go ${{ matrix.go }} test name: Go ${{ matrix.go }} test
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup go - name: Setup go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/... - run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/...
- uses: codecov/codecov-action@v3.1.4 - uses: codecov/codecov-action@v5.4.3
with: with:
file: ./profile.cov file: ./profile.cov
name: codecov-go name: codecov-go
release: release:
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
needs: [test] needs: [test]
if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }} if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }}
env: env:

View file

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

View file

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

109
README.md
View file

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

370
UPGRADING.md Normal file
View 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
```

View file

@ -13,8 +13,8 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/zitadel/oidc/v3/pkg/client/rs" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
const ( const (

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@ -12,12 +13,11 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/exp/slog"
"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/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 ( var (
@ -32,6 +32,7 @@ func main() {
issuer := os.Getenv("ISSUER") issuer := os.Getenv("ISSUER")
port := os.Getenv("PORT") port := os.Getenv("PORT")
scopes := strings.Split(os.Getenv("SCOPES"), " ") scopes := strings.Split(os.Getenv("SCOPES"), " ")
responseMode := os.Getenv("RESPONSE_MODE")
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
@ -55,6 +56,7 @@ func main() {
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)), rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
rp.WithHTTPClient(client), rp.WithHTTPClient(client),
rp.WithLogger(logger), rp.WithLogger(logger),
rp.WithSigningAlgsFromDiscovery(),
} }
if clientSecret == "" { if clientSecret == "" {
options = append(options, rp.WithPKCE(cookieHandler)) options = append(options, rp.WithPKCE(cookieHandler))
@ -77,20 +79,37 @@ func main() {
return uuid.New().String() return uuid.New().String()
} }
urlOptions := []rp.URLParamOpt{
rp.WithPromptURLParam("Welcome back!"),
}
if responseMode != "" {
urlOptions = append(urlOptions, rp.WithResponseModeURLParam(oidc.ResponseMode(responseMode)))
}
// register the AuthURLHandler at your preferred path. // register the AuthURLHandler at your preferred path.
// the AuthURLHandler creates the auth request and redirects the user to the auth server. // the AuthURLHandler creates the auth request and redirects the user to the auth server.
// including state handling with secure cookie and the possibility to use PKCE. // including state handling with secure cookie and the possibility to use PKCE.
// Prompts can optionally be set to inform the server of // Prompts can optionally be set to inform the server of
// any messages that need to be prompted back to the user. // any messages that need to be prompted back to the user.
http.Handle("/login", rp.AuthURLHandler(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 // for demonstration purposes the returned userinfo response is written as JSON object onto response
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
fmt.Println("access token", tokens.AccessToken)
fmt.Println("refresh token", tokens.RefreshToken)
fmt.Println("id token", tokens.IDToken)
data, err := json.Marshal(info) data, err := json.Marshal(info)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
w.Header().Set("content-type", "application/json")
w.Write(data) w.Write(data)
} }

View file

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

View file

@ -10,10 +10,10 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
githubOAuth "golang.org/x/oauth2/github" githubOAuth "golang.org/x/oauth2/github"
"github.com/zitadel/oidc/v3/pkg/client/rp" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/client/rp/cli" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp/cli"
"github.com/zitadel/oidc/v3/pkg/http" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
var ( var (

View file

@ -13,7 +13,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/oauth2" "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 var client = http.DefaultClient

View 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
}

View 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)
}
})
}
}

View file

@ -8,7 +8,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/zitadel/oidc/v3/pkg/op" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
) )
const ( const (

View file

@ -10,8 +10,8 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/oidc/v3/example/server/storage" "git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
"github.com/zitadel/oidc/v3/pkg/op" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
) )
const ( const (

View file

@ -8,10 +8,10 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/zitadel/oidc/v3/pkg/op"
) )
type deviceAuthenticate interface { type deviceAuthenticate interface {

View file

@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/zitadel/oidc/v3/pkg/op"
) )
type login struct { type login struct {

View file

@ -3,31 +3,22 @@ package exampleop
import ( import (
"crypto/sha256" "crypto/sha256"
"log" "log"
"log/slog"
"net/http" "net/http"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"golang.org/x/exp/slog"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/oidc/v3/example/server/storage" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
"github.com/zitadel/oidc/v3/pkg/op"
) )
const ( const (
pathLoggedOut = "/logged-out" pathLoggedOut = "/logged-out"
) )
func init() {
storage.RegisterClients(
storage.NativeClient("native"),
storage.WebClient("web", "secret"),
storage.WebClient("api", "secret"),
)
}
type Storage interface { type Storage interface {
op.Storage op.Storage
authenticate authenticate
@ -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 // for simplicity, we provide a very small default page for users who have signed out
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("signed out successfully")) 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 // 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) handler := http.Handler(provider)
if wrapServer { 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) // we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)

View file

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

View file

@ -3,8 +3,8 @@ package storage
import ( import (
"time" "time"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
) )
var ( var (
@ -184,10 +184,10 @@ func WebClient(id, secret string, redirectURIs ...string) *Client {
applicationType: op.ApplicationTypeWeb, applicationType: op.ApplicationTypeWeb,
authMethod: oidc.AuthMethodBasic, authMethod: oidc.AuthMethodBasic,
loginURL: defaultLoginURL, loginURL: defaultLoginURL,
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode, oidc.ResponseTypeIDTokenOnly, oidc.ResponseTypeIDToken},
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken, oidc.GrantTypeTokenExchange}, grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken, oidc.GrantTypeTokenExchange},
accessTokenType: op.AccessTokenTypeBearer, accessTokenType: op.AccessTokenTypeBearer,
devMode: false, devMode: true,
idTokenUserinfoClaimsAssertion: false, idTokenUserinfoClaimsAssertion: false,
clockSkew: 0, clockSkew: 0,
} }

View file

@ -1,13 +1,13 @@
package storage package storage
import ( import (
"log/slog"
"time" "time"
"golang.org/x/exp/slog"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
) )
const ( const (
@ -35,6 +35,7 @@ type AuthRequest struct {
UserID string UserID string
Scopes []string Scopes []string
ResponseType oidc.ResponseType ResponseType oidc.ResponseType
ResponseMode oidc.ResponseMode
Nonce string Nonce string
CodeChallenge *OIDCCodeChallenge CodeChallenge *OIDCCodeChallenge
@ -100,7 +101,7 @@ func (a *AuthRequest) GetResponseType() oidc.ResponseType {
} }
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
return "" // we won't handle response mode in this example return a.ResponseMode
} }
func (a *AuthRequest) GetScopes() []string { func (a *AuthRequest) GetScopes() []string {
@ -120,7 +121,7 @@ func (a *AuthRequest) Done() bool {
} }
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string { func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
prompts := make([]string, len(oidcPrompt)) prompts := make([]string, 0, len(oidcPrompt))
for _, oidcPrompt := range oidcPrompt { for _, oidcPrompt := range oidcPrompt {
switch oidcPrompt { switch oidcPrompt {
case oidc.PromptNone, case oidc.PromptNone,
@ -154,6 +155,7 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques
UserID: userID, UserID: userID,
Scopes: authReq.Scopes, Scopes: authReq.Scopes,
ResponseType: authReq.ResponseType, ResponseType: authReq.ResponseType,
ResponseMode: authReq.ResponseMode,
Nonce: authReq.Nonce, Nonce: authReq.Nonce,
CodeChallenge: &OIDCCodeChallenge{ CodeChallenge: &OIDCCodeChallenge{
Challenge: authReq.CodeChallenge, Challenge: authReq.CodeChallenge,
@ -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 { type OIDCCodeChallenge struct {
Challenge string Challenge string
Method string Method string

View file

@ -11,11 +11,11 @@ import (
"sync" "sync"
"time" "time"
jose "github.com/go-jose/go-jose/v3" jose "github.com/go-jose/go-jose/v4"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
) )
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant // serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
@ -90,6 +90,10 @@ func (s *publicKey) Key() any {
} }
func NewStorage(userStore UserStore) *Storage { func NewStorage(userStore UserStore) *Storage {
return NewStorageWithClients(userStore, clients)
}
func NewStorageWithClients(userStore UserStore, clients map[string]*Client) *Storage {
key, _ := rsa.GenerateKey(rand.Reader, 2048) key, _ := rsa.GenerateKey(rand.Reader, 2048)
return &Storage{ return &Storage{
authRequests: make(map[string]*AuthRequest), authRequests: make(map[string]*AuthRequest),
@ -147,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 // in this example we'll simply check the username / password and set a boolean to true
// therefore we will also just check this boolean if the request / login has been finished // therefore we will also just check this boolean if the request / login has been finished
request.done = true request.done = true
request.authTime = time.Now()
return nil return nil
} }
return fmt.Errorf("username or password wrong") return fmt.Errorf("username or password wrong")
@ -291,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 // if we get here, the currentRefreshToken was not empty, so the call is a refresh token request
// we therefore will have to check the currentRefreshToken and renew the refresh token // we therefore will have to check the currentRefreshToken and renew the refresh token
refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken)
newRefreshToken = uuid.NewString()
accessToken, err := s.accessToken(applicationID, newRefreshToken, request.GetSubject(), request.GetAudience(), request.GetScopes())
if err != nil { if err != nil {
return "", "", time.Time{}, err return "", "", time.Time{}, err
} }
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
if err != nil { if err := s.renewRefreshToken(currentRefreshToken, newRefreshToken, accessToken.ID); err != nil {
return "", "", time.Time{}, err return "", "", time.Time{}, err
} }
return accessToken.ID, refreshToken, accessToken.Expiration, nil
return accessToken.ID, newRefreshToken, accessToken.Expiration, nil
} }
func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
@ -381,14 +392,9 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID
if refreshToken.ApplicationID != clientID { if refreshToken.ApplicationID != clientID {
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
} }
// if it is a refresh token, you will have to remove the access token as well
delete(s.refreshTokens, refreshToken.ID) delete(s.refreshTokens, refreshToken.ID)
for _, accessToken := range s.tokens { // if it is a refresh token, you will have to remove the access token as well
if accessToken.RefreshTokenID == refreshToken.ID { delete(s.tokens, refreshToken.AccessToken)
delete(s.tokens, accessToken.ID)
return nil
}
}
return nil return nil
} }
@ -484,6 +490,9 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserI
// return err // return err
// } // }
//} //}
if token.Expiration.Before(time.Now()) {
return fmt.Errorf("token is expired")
}
return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes) return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes)
} }
@ -590,33 +599,41 @@ func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime
Audience: accessToken.Audience, Audience: accessToken.Audience,
Expiration: time.Now().Add(5 * time.Hour), Expiration: time.Now().Add(5 * time.Hour),
Scopes: accessToken.Scopes, Scopes: accessToken.Scopes,
AccessToken: accessToken.ID,
} }
s.refreshTokens[token.ID] = token s.refreshTokens[token.ID] = token
return token.Token, nil return token.Token, nil
} }
// renewRefreshToken checks the provided refresh_token and creates a new one based on the current // renewRefreshToken checks the provided refresh_token and creates a new one based on the current
func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) { //
// [Refresh Token Rotation] is implemented.
//
// [Refresh Token Rotation]: https://www.rfc-editor.org/rfc/rfc6819#section-5.2.2.3
func (s *Storage) renewRefreshToken(currentRefreshToken, newRefreshToken, newAccessToken string) error {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
refreshToken, ok := s.refreshTokens[currentRefreshToken] refreshToken, ok := s.refreshTokens[currentRefreshToken]
if !ok { if !ok {
return "", "", fmt.Errorf("invalid refresh token") return fmt.Errorf("invalid refresh token")
} }
// deletes the refresh token and all access tokens which were issued based on this refresh token // deletes the refresh token
delete(s.refreshTokens, currentRefreshToken) delete(s.refreshTokens, currentRefreshToken)
for _, token := range s.tokens {
if token.RefreshTokenID == currentRefreshToken { // delete the access token which was issued based on this refresh token
delete(s.tokens, token.ID) delete(s.tokens, refreshToken.AccessToken)
break
} if refreshToken.Expiration.Before(time.Now()) {
return fmt.Errorf("expired refresh token")
} }
// creates a new refresh token based on the current one // creates a new refresh token based on the current one
token := uuid.NewString() refreshToken.Token = newRefreshToken
refreshToken.Token = token refreshToken.ID = newRefreshToken
refreshToken.ID = token refreshToken.Expiration = time.Now().Add(5 * time.Hour)
s.refreshTokens[token] = refreshToken refreshToken.AccessToken = newAccessToken
return token, refreshToken.ID, nil s.refreshTokens[newRefreshToken] = refreshToken
return nil
} }
// accessToken will store an access_token in-memory based on the provided information // accessToken will store an access_token in-memory based on the provided information

View file

@ -4,10 +4,10 @@ import (
"context" "context"
"time" "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/oidc"
"github.com/zitadel/oidc/v3/pkg/op" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
) )
type multiStorage struct { type multiStorage struct {

View file

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

View file

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

View file

@ -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)
}
})
}
}

45
go.mod
View file

@ -1,41 +1,40 @@
module github.com/zitadel/oidc/v3 module git.christmann.info/LARA/zitadel-oidc/v3
go 1.19 go 1.23.7
toolchain go1.24.1
require ( require (
github.com/go-chi/chi/v5 v5.0.10 github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/go-jose/go-jose/v3 v3.0.1 github.com/go-chi/chi/v5 v5.2.1
github.com/go-jose/go-jose/v4 v4.0.5
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/google/go-github/v31 v31.0.0 github.com/google/go-github/v31 v31.0.0
github.com/google/uuid v1.4.0 github.com/google/uuid v1.6.0
github.com/gorilla/securecookie v1.1.2 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/gu v0.3.1
github.com/muhlemmer/httpforwarded v0.1.0 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/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.10.0
github.com/zitadel/logging v0.5.0 github.com/zitadel/logging v0.6.2
github.com/zitadel/schema v1.3.0 github.com/zitadel/schema v1.3.1
go.opentelemetry.io/otel v1.21.0 go.opentelemetry.io/otel v1.29.0
go.opentelemetry.io/otel/trace v1.21.0 golang.org/x/oauth2 v0.30.0
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/text v0.26.0
golang.org/x/oauth2 v0.14.0
golang.org/x/text v0.14.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.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/google/go-querystring v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect
golang.org/x/crypto v0.15.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/net v0.18.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sys v0.14.0 // indirect golang.org/x/net v0.38.0 // indirect
google.golang.org/appengine v1.6.7 // indirect golang.org/x/sys v0.31.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

96
go.sum
View file

@ -1,85 +1,80 @@
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= 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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 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.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA=
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= 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 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 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 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA= github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU=
github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4=
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU=
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 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.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -88,14 +83,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
@ -104,15 +98,11 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -8,8 +8,8 @@ import (
"fmt" "fmt"
"os" "os"
tu "github.com/zitadel/oidc/v3/internal/testutil" tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
var custom = map[string]any{ var custom = map[string]any{

View file

@ -8,9 +8,9 @@ import (
"errors" "errors"
"time" "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/muhlemmer/gu"
"github.com/zitadel/oidc/v3/pkg/oidc"
) )
// KeySet implements oidc.Keys // KeySet implements oidc.Keys

View file

@ -2,7 +2,6 @@ package client
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -11,20 +10,27 @@ import (
"strings" "strings"
"time" "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" "golang.org/x/oauth2"
"github.com/zitadel/logging" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
"github.com/zitadel/oidc/v3/pkg/crypto" httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
httphelper "github.com/zitadel/oidc/v3/pkg/http" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/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 // Discover calls the discovery endpoint of the provided issuer and returns its configuration
// It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url // It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url
func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellKnownUrl ...string) (*oidc.DiscoveryConfiguration, error) { func Discover(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 wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" { if len(wellKnownUrl) == 1 && wellKnownUrl[0] != "" {
wellKnown = wellKnownUrl[0] wellKnown = wellKnownUrl[0]
@ -36,7 +42,7 @@ func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellK
discoveryConfig := new(oidc.DiscoveryConfiguration) discoveryConfig := new(oidc.DiscoveryConfiguration)
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig) err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
if err != nil { if err != nil {
return nil, err return nil, errors.Join(oidc.ErrDiscoveryFailed, err)
} }
if logger, ok := logging.FromContext(ctx); ok { if logger, ok := logging.FromContext(ctx); ok {
logger.Debug("discover", "config", discoveryConfig) 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) { 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) req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn)
if err != nil { if err != nil {
return nil, err return nil, err
@ -86,7 +95,15 @@ type EndSessionCaller interface {
} }
func CallEndSessionEndpoint(ctx context.Context, request any, authFn any, caller EndSessionCaller) (*url.URL, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -129,7 +146,15 @@ type RevokeRequest struct {
} }
func CallRevokeEndpoint(ctx context.Context, request any, authFn any, caller RevokeCaller) error { 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 { if err != nil {
return err 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) { 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) req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, authFn)
if err != nil { if err != nil {
return nil, err 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) { func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
privateKey, err := crypto.BytesToPrivateKey(key) privateKey, algorithm, err := crypto.BytesToPrivateKey(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
signingKey := jose.SigningKey{ signingKey := jose.SigningKey{
Algorithm: jose.RS256, Algorithm: algorithm,
Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID}, Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID},
} }
return jose.NewSigner(signingKey, &jose.SignerOptions{}) return jose.NewSigner(signingKey, &jose.SignerOptions{})
@ -198,7 +226,15 @@ type DeviceAuthorizationCaller interface {
} }
func CallDeviceAuthorizationEndpoint(ctx context.Context, request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller, authFn any) (*oidc.DeviceAuthorizationResponse, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -219,6 +255,9 @@ type DeviceAccessTokenRequest struct {
} }
func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
ctx, span := Tracer.Start(ctx, "CallDeviceAccessTokenEndpoint")
defer span.End()
req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, nil) req, err := httphelper.FormRequest(ctx, caller.TokenEndpoint(), request, Encoder, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -227,28 +266,17 @@ func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTok
req.SetBasicAuth(request.ClientID, request.ClientSecret) req.SetBasicAuth(request.ClientID, request.ClientSecret)
} }
httpResp, err := caller.HttpClient().Do(req) resp := new(oidc.AccessTokenResponse)
if err != nil { if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil {
return nil, err return nil, err
} }
defer httpResp.Body.Close() return resp, nil
resp := new(struct {
*oidc.AccessTokenResponse
*oidc.Error
})
if err = json.NewDecoder(httpResp.Body).Decode(resp); err != nil {
return nil, err
}
if httpResp.StatusCode == http.StatusOK {
return resp.AccessTokenResponse, nil
}
return nil, resp.Error
} }
func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) { func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
ctx, span := Tracer.Start(ctx, "PollDeviceAccessTokenEndpoint")
defer span.End()
for { for {
timer := time.After(interval) timer := time.After(interval)
select { select {

View file

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"testing" "testing"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -22,7 +23,7 @@ func TestDiscover(t *testing.T) {
name string name string
args args args args
wantFields *wantFields wantFields *wantFields
wantErr bool wantErr error
}{ }{
{ {
name: "spotify", // https://github.com/zitadel/oidc/issues/406 name: "spotify", // https://github.com/zitadel/oidc/issues/406
@ -32,17 +33,20 @@ func TestDiscover(t *testing.T) {
wantFields: &wantFields{ wantFields: &wantFields{
UILocalesSupported: true, UILocalesSupported: true,
}, },
wantErr: false, wantErr: nil,
},
{
name: "discovery failed",
args: args{
issuer: "https://example.com",
},
wantErr: oidc.ErrDiscoveryFailed,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := Discover(context.Background(), tt.args.issuer, http.DefaultClient, tt.args.wellKnownUrl...) got, err := Discover(context.Background(), tt.args.issuer, http.DefaultClient, tt.args.wellKnownUrl...)
if tt.wantErr { require.ErrorIs(t, err, tt.wantErr)
assert.Error(t, err)
return
}
require.NoError(t, err)
if tt.wantFields == nil { if tt.wantFields == nil {
return return
} }

5
pkg/client/errors.go Normal file
View file

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

View file

@ -5,6 +5,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log/slog"
"math/rand" "math/rand"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
@ -20,16 +21,16 @@ import (
"github.com/jeremija/gosubmit" "github.com/jeremija/gosubmit"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/exp/slog" "golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/example/server/exampleop" "git.christmann.info/LARA/zitadel-oidc/v3/example/server/exampleop"
"github.com/zitadel/oidc/v3/example/server/storage" "git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
"github.com/zitadel/oidc/v3/pkg/client/rp" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/client/rs" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs"
"github.com/zitadel/oidc/v3/pkg/client/tokenexchange" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/tokenexchange"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
) )
var Logger = slog.New( var Logger = slog.New(
@ -110,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) { func TestResourceServerTokenExchange(t *testing.T) {
for _, wrapServer := range []bool{false, true} { for _, wrapServer := range []bool{false, true} {
t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) { t.Run(fmt.Sprint("wrapServer ", wrapServer), func(t *testing.T) {
@ -217,6 +304,7 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
targetURL, targetURL,
[]string{"openid", "email", "profile", "offline_access"}, []string{"openid", "email", "profile", "offline_access"},
rp.WithPKCE(cookieHandler), rp.WithPKCE(cookieHandler),
rp.WithAuthStyle(oauth2.AuthStyleInHeader),
rp.WithVerifierOpts( rp.WithVerifierOpts(
rp.WithIssuedAtOffset(5*time.Second), rp.WithIssuedAtOffset(5*time.Second),
rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"), rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"),
@ -323,6 +411,31 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
return provider, tokens return provider, tokens
} }
func TestClientCredentials(t *testing.T) {
targetURL := "http://local-site"
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
var dh deferredHandler
opServer := httptest.NewServer(&dh)
defer opServer.Close()
t.Logf("auth server at %s", opServer.URL)
dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, Logger, true)
provider, err := rp.NewRelyingPartyOIDC(
CTX,
opServer.URL,
"sid1",
"verysecret",
targetURL,
[]string{"openid"},
)
require.NoError(t, err, "new rp")
token, err := rp.ClientCredentials(CTX, provider, nil)
require.NoError(t, err, "ClientCredentials call")
require.NotNil(t, token)
assert.NotEmpty(t, token.AccessToken)
}
func TestErrorFromPromptNone(t *testing.T) { func TestErrorFromPromptNone(t *testing.T) {
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
require.NoError(t, err, "create cookie jar") require.NoError(t, err, "create cookie jar")

View file

@ -6,8 +6,8 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/pkg/http" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
// JWTProfileExchange handles the oauth2 jwt profile exchange // JWTProfileExchange handles the oauth2 jwt profile exchange

View file

@ -2,7 +2,7 @@ package client
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "os"
) )
const ( const (
@ -24,7 +24,7 @@ type KeyFile struct {
} }
func ConfigFromKeyFile(path string) (*KeyFile, error) { func ConfigFromKeyFile(path string) (*KeyFile, error) {
data, err := ioutil.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -5,11 +5,11 @@ import (
"net/http" "net/http"
"time" "time"
jose "github.com/go-jose/go-jose/v3" jose "github.com/go-jose/go-jose/v4"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/pkg/client" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
type TokenSource interface { type TokenSource interface {

View file

@ -4,9 +4,9 @@ import (
"context" "context"
"net/http" "net/http"
"github.com/zitadel/oidc/v3/pkg/client/rp" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
const ( const (

View file

@ -1,7 +1,7 @@
package rp package rp
import ( 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 // DelegationTokenRequest is an implementation of TokenExchangeRequest

View file

@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/zitadel/oidc/v3/pkg/client" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) { 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: // in RFC 8628, section 3.1 and 3.2:
// https://www.rfc-editor.org/rfc/rfc8628#section-3.1 // https://www.rfc-editor.org/rfc/rfc8628#section-3.1
func DeviceAuthorization(ctx context.Context, scopes []string, rp RelyingParty, authFn any) (*oidc.DeviceAuthorizationResponse, error) { func DeviceAuthorization(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") ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAuthorization")
req, err := newDeviceClientCredentialsRequest(scopes, rp) req, err := newDeviceClientCredentialsRequest(scopes, rp)
if err != nil { 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: // by means of polling as defined in RFC, section 3.3 and 3.4:
// https://www.rfc-editor.org/rfc/rfc8628#section-3.4 // https://www.rfc-editor.org/rfc/rfc8628#section-3.4
func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) { func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) {
ctx, span := client.Tracer.Start(ctx, "DeviceAccessToken")
defer span.End()
ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAccessToken") ctx = logCtxWithRPData(ctx, rp, "function", "DeviceAccessToken")
req := &client.DeviceAccessTokenRequest{ req := &client.DeviceAccessTokenRequest{
DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{ DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{

5
pkg/client/rp/errors.go Normal file
View file

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

View file

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

View file

@ -2,9 +2,9 @@ package rp
import ( import (
"context" "context"
"log/slog"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"golang.org/x/exp/slog"
) )
func logCtxWithRPData(ctx context.Context, rp RelyingParty, attrs ...any) context.Context { func logCtxWithRPData(ctx context.Context, rp RelyingParty, attrs ...any) context.Context {

View file

@ -4,20 +4,20 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "log/slog"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
jose "github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v4"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/zitadel/logging"
"golang.org/x/exp/slog"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"github.com/zitadel/oidc/v3/pkg/client" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/zitadel/logging"
) )
const ( const (
@ -60,42 +60,55 @@ type RelyingParty interface {
// UserinfoEndpoint returns the userinfo // UserinfoEndpoint returns the userinfo
UserinfoEndpoint() string UserinfoEndpoint() string
// GetDeviceAuthorizationEndpoint returns the enpoint which can // GetDeviceAuthorizationEndpoint returns the endpoint which can
// be used to start a DeviceAuthorization flow. // be used to start a DeviceAuthorization flow.
GetDeviceAuthorizationEndpoint() string GetDeviceAuthorizationEndpoint() string
// IDTokenVerifier returns the verifier used for oidc id_token verification // IDTokenVerifier returns the verifier used for oidc id_token verification
IDTokenVerifier() *IDTokenVerifier IDTokenVerifier() *IDTokenVerifier
// ErrorHandler returns the handler used for callback errors
// ErrorHandler returns the handler used for callback errors
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
// Logger from the context, or a fallback if set. // Logger from the context, or a fallback if set.
Logger(context.Context) (logger *slog.Logger, ok bool) Logger(context.Context) (logger *slog.Logger, ok bool)
} }
type HasUnauthorizedHandler interface {
// UnauthorizedHandler returns the handler used for unauthorized errors
UnauthorizedHandler() func(w http.ResponseWriter, r *http.Request, desc string, state string)
}
type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string)
type UnauthorizedHandler func(w http.ResponseWriter, r *http.Request, desc string, state string)
var DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { var DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError) http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError)
} }
var DefaultUnauthorizedHandler UnauthorizedHandler = func(w http.ResponseWriter, r *http.Request, desc string, state string) {
http.Error(w, desc, http.StatusUnauthorized)
}
type relyingParty struct { type relyingParty struct {
issuer string issuer string
DiscoveryEndpoint string DiscoveryEndpoint string
endpoints Endpoints endpoints Endpoints
oauthConfig *oauth2.Config oauthConfig *oauth2.Config
oauth2Only bool oauth2Only bool
pkce bool pkce bool
useSigningAlgsFromDiscovery bool
httpClient *http.Client httpClient *http.Client
cookieHandler *httphelper.CookieHandler cookieHandler *httphelper.CookieHandler
errorHandler func(http.ResponseWriter, *http.Request, string, string, string) oauthAuthStyle oauth2.AuthStyle
idTokenVerifier *IDTokenVerifier
verifierOpts []VerifierOption errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
signer jose.Signer unauthorizedHandler func(http.ResponseWriter, *http.Request, string, string)
logger *slog.Logger idTokenVerifier *IDTokenVerifier
verifierOpts []VerifierOption
signer jose.Signer
logger *slog.Logger
} }
func (rp *relyingParty) OAuthConfig() *oauth2.Config { func (rp *relyingParty) OAuthConfig() *oauth2.Config {
@ -156,6 +169,13 @@ func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request,
return rp.errorHandler return rp.errorHandler
} }
func (rp *relyingParty) UnauthorizedHandler() func(http.ResponseWriter, *http.Request, string, string) {
if rp.unauthorizedHandler == nil {
rp.unauthorizedHandler = DefaultUnauthorizedHandler
}
return rp.unauthorizedHandler
}
func (rp *relyingParty) Logger(ctx context.Context) (logger *slog.Logger, ok bool) { func (rp *relyingParty) Logger(ctx context.Context) (logger *slog.Logger, ok bool) {
logger, ok = logging.FromContext(ctx) logger, ok = logging.FromContext(ctx)
if ok { if ok {
@ -169,9 +189,11 @@ func (rp *relyingParty) Logger(ctx context.Context) (logger *slog.Logger, ok boo
// it will use the AuthURL and TokenURL set in config // it will use the AuthURL and TokenURL set in config
func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) { func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) {
rp := &relyingParty{ rp := &relyingParty{
oauthConfig: config, oauthConfig: config,
httpClient: httphelper.DefaultHTTPClient, httpClient: httphelper.DefaultHTTPClient,
oauth2Only: true, oauth2Only: true,
unauthorizedHandler: DefaultUnauthorizedHandler,
oauthAuthStyle: oauth2.AuthStyleAutoDetect,
} }
for _, optFunc := range options { for _, optFunc := range options {
@ -180,9 +202,12 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart
} }
} }
rp.oauthConfig.Endpoint.AuthStyle = rp.oauthAuthStyle
// avoid races by calling these early // avoid races by calling these early
_ = rp.IDTokenVerifier() // sets idTokenVerifier _ = rp.IDTokenVerifier() // sets idTokenVerifier
_ = rp.ErrorHandler() // sets errorHandler _ = rp.ErrorHandler() // sets errorHandler
_ = rp.UnauthorizedHandler() // sets unauthorizedHandler
return rp, nil return rp, nil
} }
@ -199,8 +224,9 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
RedirectURL: redirectURI, RedirectURL: redirectURI,
Scopes: scopes, Scopes: scopes,
}, },
httpClient: httphelper.DefaultHTTPClient, httpClient: httphelper.DefaultHTTPClient,
oauth2Only: false, oauth2Only: false,
oauthAuthStyle: oauth2.AuthStyleAutoDetect,
} }
for _, optFunc := range options { for _, optFunc := range options {
@ -213,13 +239,20 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rp.useSigningAlgsFromDiscovery {
rp.verifierOpts = append(rp.verifierOpts, WithSupportedSigningAlgorithms(discoveryConfiguration.IDTokenSigningAlgValuesSupported...))
}
endpoints := GetEndpoints(discoveryConfiguration) endpoints := GetEndpoints(discoveryConfiguration)
rp.oauthConfig.Endpoint = endpoints.Endpoint rp.oauthConfig.Endpoint = endpoints.Endpoint
rp.endpoints = endpoints rp.endpoints = endpoints
rp.oauthConfig.Endpoint.AuthStyle = rp.oauthAuthStyle
rp.endpoints.Endpoint.AuthStyle = rp.oauthAuthStyle
// avoid races by calling these early // avoid races by calling these early
_ = rp.IDTokenVerifier() // sets idTokenVerifier _ = rp.IDTokenVerifier() // sets idTokenVerifier
_ = rp.ErrorHandler() // sets errorHandler _ = rp.ErrorHandler() // sets errorHandler
_ = rp.UnauthorizedHandler() // sets unauthorizedHandler
return rp, nil return rp, nil
} }
@ -268,6 +301,20 @@ func WithErrorHandler(errorHandler ErrorHandler) Option {
} }
} }
func WithUnauthorizedHandler(unauthorizedHandler UnauthorizedHandler) Option {
return func(rp *relyingParty) error {
rp.unauthorizedHandler = unauthorizedHandler
return nil
}
}
func WithAuthStyle(oauthAuthStyle oauth2.AuthStyle) Option {
return func(rp *relyingParty) error {
rp.oauthAuthStyle = oauthAuthStyle
return nil
}
}
func WithVerifierOpts(opts ...VerifierOption) Option { func WithVerifierOpts(opts ...VerifierOption) Option {
return func(rp *relyingParty) error { return func(rp *relyingParty) error {
rp.verifierOpts = opts rp.verifierOpts = opts
@ -305,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) type SignerFromKey func() (jose.Signer, error)
func SignerFromKeyPath(path string) SignerFromKey { func SignerFromKeyPath(path string) SignerFromKey {
@ -345,7 +401,7 @@ func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string {
// AuthURLHandler extends the `AuthURL` method with a http redirect handler // AuthURLHandler extends the `AuthURL` method with a http redirect handler
// including handling setting cookie for secure `state` transfer. // including handling setting cookie for secure `state` transfer.
// Custom 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 { func AuthURLHandler(stateFn func() string, rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
opts := make([]AuthURLOpt, len(urlParam)) opts := make([]AuthURLOpt, len(urlParam))
@ -355,13 +411,13 @@ func AuthURLHandler(stateFn func() string, rp RelyingParty, urlParam ...URLParam
state := stateFn() state := stateFn()
if err := trySetStateCookie(w, state, rp); err != nil { if err := trySetStateCookie(w, state, rp); err != nil {
http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized) unauthorizedError(w, r, "failed to create state cookie: "+err.Error(), state, rp)
return return
} }
if rp.IsPKCE() { if rp.IsPKCE() {
codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp) codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp)
if err != nil { if err != nil {
http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized) unauthorizedError(w, r, "failed to create code challenge: "+err.Error(), state, rp)
return return
} }
opts = append(opts, WithCodeChallenge(codeChallenge)) opts = append(opts, WithCodeChallenge(codeChallenge))
@ -385,6 +441,9 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri
var ErrMissingIDToken = errors.New("id_token missing") var ErrMissingIDToken = errors.New("id_token missing")
func verifyTokenResponse[C oidc.IDClaims](ctx context.Context, token *oauth2.Token, rp RelyingParty) (*oidc.Tokens[C], error) { 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() { if rp.IsOAuth2Only() {
return &oidc.Tokens[C]{Token: token}, nil return &oidc.Tokens[C]{Token: token}, nil
} }
@ -402,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 // CodeExchange handles the oauth2 code exchange, extracting and validating the id_token
// returning it parsed together with the oauth2 tokens (access, refresh) // returning it parsed together with the oauth2 tokens (access, refresh)
func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) { func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) {
ctx, codeExchangeSpan := client.Tracer.Start(ctx, "CodeExchange")
defer codeExchangeSpan.End()
ctx = logCtxWithRPData(ctx, rp, "function", "CodeExchange") ctx = logCtxWithRPData(ctx, rp, "function", "CodeExchange")
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
codeOpts := make([]oauth2.AuthCodeOption, 0) codeOpts := make([]oauth2.AuthCodeOption, 0)
@ -409,29 +471,59 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP
codeOpts = append(codeOpts, opt()...) codeOpts = append(codeOpts, opt()...)
} }
ctx, oauthExchangeSpan := client.Tracer.Start(ctx, "OAuthExchange")
token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...) token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
oauthExchangeSpan.End()
return verifyTokenResponse[C](ctx, token, rp) return verifyTokenResponse[C](ctx, token, rp)
} }
// ClientCredentials requests an access token using the `client_credentials` grant,
// as defined in [RFC 6749, section 4.4].
//
// As there is no user associated to the request an ID Token can never be returned.
// Client Credentials are undefined in OpenID Connect and is a pure OAuth2 grant.
// Furthermore the server SHOULD NOT return a refresh token.
//
// [RFC 6749, section 4.4]: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
func ClientCredentials(ctx context.Context, rp RelyingParty, endpointParams url.Values) (token *oauth2.Token, err error) {
ctx = logCtxWithRPData(ctx, rp, "function", "ClientCredentials")
ctx, span := client.Tracer.Start(ctx, "ClientCredentials")
defer span.End()
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
config := clientcredentials.Config{
ClientID: rp.OAuthConfig().ClientID,
ClientSecret: rp.OAuthConfig().ClientSecret,
TokenURL: rp.OAuthConfig().Endpoint.TokenURL,
Scopes: rp.OAuthConfig().Scopes,
EndpointParams: endpointParams,
AuthStyle: rp.OAuthConfig().Endpoint.AuthStyle,
}
return config.Token(ctx)
}
type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty)
// CodeExchangeHandler extends the `CodeExchange` method with a http handler // CodeExchangeHandler extends the `CodeExchange` method with a http handler
// including cookie handling for secure `state` transfer // including cookie handling for secure `state` transfer
// and optional PKCE code verifier checking. // and optional PKCE code verifier checking.
// Custom paramaters can optionally be set to the token URL. // Custom parameters can optionally be set to the token URL.
func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc { func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx, span := client.Tracer.Start(r.Context(), "CodeExchangeHandler")
r = r.WithContext(ctx)
defer span.End()
state, err := tryReadStateCookie(w, r, rp) state, err := tryReadStateCookie(w, r, rp)
if err != nil { if err != nil {
http.Error(w, "failed to get state: "+err.Error(), http.StatusUnauthorized) unauthorizedError(w, r, "failed to get state: "+err.Error(), state, rp)
return return
} }
params := r.URL.Query() if errValue := r.FormValue("error"); errValue != "" {
if params.Get("error") != "" { rp.ErrorHandler()(w, r, errValue, r.FormValue("error_description"), state)
rp.ErrorHandler()(w, r, params.Get("error"), params.Get("error_description"), state)
return return
} }
codeOpts := make([]CodeExchangeOpt, len(urlParam)) codeOpts := make([]CodeExchangeOpt, len(urlParam))
@ -442,23 +534,23 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R
if rp.IsPKCE() { if rp.IsPKCE() {
codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode) codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode)
if err != nil { if err != nil {
http.Error(w, "failed to get code verifier: "+err.Error(), http.StatusUnauthorized) unauthorizedError(w, r, "failed to get code verifier: "+err.Error(), state, rp)
return return
} }
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
rp.CookieHandler().DeleteCookie(w, pkceCode) rp.CookieHandler().DeleteCookie(w, pkceCode)
} }
if rp.Signer() != nil { 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 { if err != nil {
http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) unauthorizedError(w, r, "failed to build assertion: "+err.Error(), state, rp)
return return
} }
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) 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 { if err != nil {
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) unauthorizedError(w, r, "failed to exchange token: "+err.Error(), state, rp)
return return
} }
callback(w, r, tokens, state, rp) callback(w, r, tokens, state, rp)
@ -476,9 +568,13 @@ type CodeExchangeUserinfoCallback[C oidc.IDClaims, U SubjectGetter] func(w http.
// on success it will pass the userinfo into its callback function as well // on success it will pass the userinfo into its callback function as well
func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCallback[C, U]) CodeExchangeCallback[C] { func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCallback[C, U]) CodeExchangeCallback[C] {
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) { return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) {
ctx, span := client.Tracer.Start(r.Context(), "UserinfoCallback")
r = r.WithContext(ctx)
defer span.End()
info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp) info, err := Userinfo[U](r.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
if err != nil { if err != nil {
http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized) unauthorizedError(w, r, "userinfo failed: "+err.Error(), state, rp)
return return
} }
f(w, r, tokens, state, rp, info) f(w, r, tokens, state, rp, info)
@ -494,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) { func Userinfo[U SubjectGetter](ctx context.Context, token, tokenType, subject string, rp RelyingParty) (userinfo U, err error) {
var nilU U var nilU U
ctx = logCtxWithRPData(ctx, rp, "function", "Userinfo") 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) req, err := http.NewRequestWithContext(ctx, http.MethodGet, rp.UserinfoEndpoint(), nil)
if err != nil { if err != nil {
@ -545,9 +643,8 @@ type Endpoints struct {
func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints { func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
return Endpoints{ return Endpoints{
Endpoint: oauth2.Endpoint{ Endpoint: oauth2.Endpoint{
AuthURL: discoveryConfig.AuthorizationEndpoint, AuthURL: discoveryConfig.AuthorizationEndpoint,
AuthStyle: oauth2.AuthStyleAutoDetect, TokenURL: discoveryConfig.TokenEndpoint,
TokenURL: discoveryConfig.TokenEndpoint,
}, },
IntrospectURL: discoveryConfig.IntrospectionEndpoint, IntrospectURL: discoveryConfig.IntrospectionEndpoint,
UserinfoURL: discoveryConfig.UserinfoEndpoint, UserinfoURL: discoveryConfig.UserinfoEndpoint,
@ -558,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 // This is the generalized, unexported, function used by both
// URLParamOpt and AuthURLOpt. // URLParamOpt and AuthURLOpt.
func withURLParam(key, value string) func() []oauth2.AuthCodeOption { func withURLParam(key, value string) func() []oauth2.AuthCodeOption {
@ -637,11 +734,11 @@ func (t tokenEndpointCaller) TokenEndpoint() string {
type RefreshTokenRequest struct { type RefreshTokenRequest struct {
RefreshToken string `schema:"refresh_token"` RefreshToken string `schema:"refresh_token"`
Scopes oidc.SpaceDelimitedArray `schema:"scope"` Scopes oidc.SpaceDelimitedArray `schema:"scope,omitempty"`
ClientID string `schema:"client_id"` ClientID string `schema:"client_id,omitempty"`
ClientSecret string `schema:"client_secret"` ClientSecret string `schema:"client_secret,omitempty"`
ClientAssertion string `schema:"client_assertion"` ClientAssertion string `schema:"client_assertion,omitempty"`
ClientAssertionType string `schema:"client_assertion_type"` ClientAssertionType string `schema:"client_assertion_type,omitempty"`
GrantType oidc.GrantType `schema:"grant_type"` GrantType oidc.GrantType `schema:"grant_type"`
} }
@ -650,9 +747,12 @@ type RefreshTokenRequest struct {
// the old one should be considered invalid. // the old one should be considered invalid.
// //
// In case the RP is not OAuth2 only and an IDToken was part of the response, // 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. // 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) { 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") ctx = logCtxWithRPData(ctx, rp, "function", "RefreshTokens")
request := RefreshTokenRequest{ request := RefreshTokenRequest{
RefreshToken: refreshToken, RefreshToken: refreshToken,
@ -678,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) { func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectURI, optionalState string) (*url.URL, error) {
ctx = logCtxWithRPData(ctx, rp, "function", "EndSession") ctx = logCtxWithRPData(ctx, rp, "function", "EndSession")
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
defer span.End()
request := oidc.EndSessionRequest{ request := oidc.EndSessionRequest{
IdTokenHint: idToken, IdTokenHint: idToken,
ClientID: rp.OAuthConfig().ClientID, ClientID: rp.OAuthConfig().ClientID,
@ -694,6 +797,8 @@ func EndSession(ctx context.Context, rp RelyingParty, idToken, optionalRedirectU
// tokenTypeHint should be either "id_token" or "refresh_token". // tokenTypeHint should be either "id_token" or "refresh_token".
func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHint string) error { func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHint string) error {
ctx = logCtxWithRPData(ctx, rp, "function", "RevokeToken") ctx = logCtxWithRPData(ctx, rp, "function", "RevokeToken")
ctx, span := client.Tracer.Start(ctx, "RefreshTokens")
defer span.End()
request := client.RevokeRequest{ request := client.RevokeRequest{
Token: token, Token: token,
TokenTypeHint: tokenTypeHint, TokenTypeHint: tokenTypeHint,
@ -703,5 +808,13 @@ func RevokeToken(ctx context.Context, rp RelyingParty, token string, tokenTypeHi
if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" { if rc, ok := rp.(client.RevokeCaller); ok && rc.GetRevokeEndpoint() != "" {
return client.CallRevokeEndpoint(ctx, request, nil, rc) return client.CallRevokeEndpoint(ctx, request, nil, rc)
} }
return fmt.Errorf("RelyingParty does not support RevokeCaller") return ErrRelyingPartyNotSupportRevokeCaller
}
func unauthorizedError(w http.ResponseWriter, r *http.Request, desc string, state string, rp RelyingParty) {
if rp, ok := rp.(HasUnauthorizedHandler); ok {
rp.UnauthorizedHandler()(w, r, desc, state)
return
}
http.Error(w, desc, http.StatusUnauthorized)
} }

View file

@ -5,10 +5,10 @@ import (
"testing" "testing"
"time" "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/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
tu "github.com/zitadel/oidc/v3/internal/testutil"
"github.com/zitadel/oidc/v3/pkg/oidc"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )

View file

@ -5,7 +5,7 @@ import (
"golang.org/x/oauth2" "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` // TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`

View file

@ -4,8 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/zitadel/oidc/v3/pkg/client/rp" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
type UserInfo struct { type UserInfo struct {

View file

@ -4,14 +4,18 @@ import (
"context" "context"
"time" "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 // VerifyTokens implement the Token Response Validation as defined in OIDC specification
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v *IDTokenVerifier) (claims C, err error) { func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v *IDTokenVerifier) (claims C, err error) {
ctx, span := client.Tracer.Start(ctx, "VerifyTokens")
defer span.End()
var nilClaims C var nilClaims C
claims, err = VerifyIDToken[C](ctx, idToken, v) claims, err = VerifyIDToken[C](ctx, idToken, v)
@ -27,6 +31,9 @@ func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken str
// VerifyIDToken validates the id token according to // VerifyIDToken validates the id token according to
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenVerifier) (claims C, err error) { func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenVerifier) (claims C, err error) {
ctx, span := client.Tracer.Start(ctx, "VerifyIDToken")
defer span.End()
var nilClaims C var nilClaims C
decrypted, err := oidc.DecryptToken(token) decrypted, err := oidc.DecryptToken(token)
@ -66,8 +73,10 @@ func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenV
return nilClaims, err return nilClaims, err
} }
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil { if v.Nonce != nil {
return nilClaims, err if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
return nilClaims, err
}
} }
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil { if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil {

View file

@ -5,11 +5,11 @@ import (
"testing" "testing"
"time" "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/assert"
"github.com/stretchr/testify/require" "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) { func TestVerifyTokens(t *testing.T) {
@ -100,22 +100,21 @@ func TestVerifyIDToken(t *testing.T) {
MaxAge: 2 * time.Minute, MaxAge: 2 * time.Minute,
ACR: tu.ACRVerify, ACR: tu.ACRVerify,
Nonce: func(context.Context) string { return tu.ValidNonce }, Nonce: func(context.Context) string { return tu.ValidNonce },
ClientID: tu.ValidClientID,
} }
tests := []struct { tests := []struct {
name string name string
clientID string tokenClaims func() (string, *oidc.IDTokenClaims)
tokenClaims func() (string, *oidc.IDTokenClaims) customVerifier func(verifier *IDTokenVerifier)
wantErr bool wantErr bool
}{ }{
{ {
name: "success", name: "success",
clientID: tu.ValidClientID,
tokenClaims: tu.ValidIDToken, tokenClaims: tu.ValidIDToken,
}, },
{ {
name: "custom claims", name: "custom claims",
clientID: tu.ValidClientID,
tokenClaims: func() (string, *oidc.IDTokenClaims) { tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDTokenCustom( return tu.NewIDTokenCustom(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, 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", name: "parse err",
clientID: tu.ValidClientID,
tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil }, tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil },
wantErr: true, wantErr: true,
}, },
{ {
name: "invalid signature", name: "invalid signature",
clientID: tu.ValidClientID,
tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil }, tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil },
wantErr: true, wantErr: true,
}, },
{ {
name: "empty subject", name: "empty subject",
clientID: tu.ValidClientID,
tokenClaims: func() (string, *oidc.IDTokenClaims) { tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken( return tu.NewIDToken(
tu.ValidIssuer, "", tu.ValidAudience, tu.ValidIssuer, "", tu.ValidAudience,
@ -150,8 +159,7 @@ func TestVerifyIDToken(t *testing.T) {
wantErr: true, wantErr: true,
}, },
{ {
name: "wrong issuer", name: "wrong issuer",
clientID: tu.ValidClientID,
tokenClaims: func() (string, *oidc.IDTokenClaims) { tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken( return tu.NewIDToken(
"foo", tu.ValidSubject, tu.ValidAudience, "foo", tu.ValidSubject, tu.ValidAudience,
@ -162,14 +170,15 @@ func TestVerifyIDToken(t *testing.T) {
wantErr: true, wantErr: true,
}, },
{ {
name: "wrong clientID", name: "wrong clientID",
clientID: "foo", customVerifier: func(verifier *IDTokenVerifier) {
verifier.ClientID = "foo"
},
tokenClaims: tu.ValidIDToken, tokenClaims: tu.ValidIDToken,
wantErr: true, wantErr: true,
}, },
{ {
name: "expired", name: "expired",
clientID: tu.ValidClientID,
tokenClaims: func() (string, *oidc.IDTokenClaims) { tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken( return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
@ -180,8 +189,7 @@ func TestVerifyIDToken(t *testing.T) {
wantErr: true, wantErr: true,
}, },
{ {
name: "wrong IAT", name: "wrong IAT",
clientID: tu.ValidClientID,
tokenClaims: func() (string, *oidc.IDTokenClaims) { tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken( return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
@ -192,8 +200,7 @@ func TestVerifyIDToken(t *testing.T) {
wantErr: true, wantErr: true,
}, },
{ {
name: "wrong acr", name: "wrong acr",
clientID: tu.ValidClientID,
tokenClaims: func() (string, *oidc.IDTokenClaims) { tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken( return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
@ -204,8 +211,7 @@ func TestVerifyIDToken(t *testing.T) {
wantErr: true, wantErr: true,
}, },
{ {
name: "expired auth", name: "expired auth",
clientID: tu.ValidClientID,
tokenClaims: func() (string, *oidc.IDTokenClaims) { tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken( return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
@ -216,8 +222,7 @@ func TestVerifyIDToken(t *testing.T) {
wantErr: true, wantErr: true,
}, },
{ {
name: "wrong nonce", name: "wrong nonce",
clientID: tu.ValidClientID,
tokenClaims: func() (string, *oidc.IDTokenClaims) { tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken( return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience, tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
@ -231,7 +236,10 @@ func TestVerifyIDToken(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
token, want := tt.tokenClaims() token, want := tt.tokenClaims()
verifier.ClientID = tt.clientID if tt.customVerifier != nil {
tt.customVerifier(verifier)
}
got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier) got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)

View file

@ -4,9 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
tu "github.com/zitadel/oidc/v3/internal/testutil" tu "git.christmann.info/LARA/zitadel-oidc/v3/internal/testutil"
"github.com/zitadel/oidc/v3/pkg/client/rp" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
// MyCustomClaims extends the TokenClaims base, // MyCustomClaims extends the TokenClaims base,

View file

@ -4,8 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/zitadel/oidc/v3/pkg/client/rs" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client/rs"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
type IntrospectionResponse struct { type IntrospectionResponse struct {

View file

@ -6,9 +6,9 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/zitadel/oidc/v3/pkg/client" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
type ResourceServer interface { type ResourceServer interface {
@ -123,6 +123,9 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option {
// //
// [RFC7662]: https://www.rfc-editor.org/rfc/rfc7662 // [RFC7662]: https://www.rfc-editor.org/rfc/rfc7662
func Introspect[R any](ctx context.Context, rp ResourceServer, token string) (resp R, err error) { 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() == "" { if rp.IntrospectionURL() == "" {
return resp, errors.New("resource server: introspection URL is empty") return resp, errors.New("resource server: introspection URL is empty")
} }

View file

@ -4,9 +4,9 @@ import (
"context" "context"
"testing" "testing"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/oidc"
) )
func TestNewResourceServer(t *testing.T) { func TestNewResourceServer(t *testing.T) {
@ -201,7 +201,7 @@ func TestIntrospect(t *testing.T) {
{ {
name: "missing-introspect-url", name: "missing-introspect-url",
args: args{ args: args{
ctx: nil, ctx: context.Background(),
rp: rp, rp: rp,
token: "my-token", token: "my-token",
}, },

View file

@ -4,10 +4,12 @@ import (
"context" "context"
"errors" "errors"
"net/http" "net/http"
"time"
"github.com/zitadel/oidc/v3/pkg/client" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/client"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/go-jose/go-jose/v4"
) )
type TokenExchanger interface { type TokenExchanger interface {
@ -33,6 +35,17 @@ func NewTokenExchangerClientCredentials(ctx context.Context, issuer, clientID, c
return newOAuthTokenExchange(ctx, issuer, authorizer, options...) 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) { func newOAuthTokenExchange(ctx context.Context, issuer string, authorizer func() (any, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) {
te := &OAuthTokenExchange{ te := &OAuthTokenExchange{
httpClient: httphelper.DefaultHTTPClient, httpClient: httphelper.DefaultHTTPClient,
@ -101,6 +114,9 @@ func ExchangeToken(
Scopes []string, Scopes []string,
RequestedTokenType oidc.TokenType, RequestedTokenType oidc.TokenType,
) (*oidc.TokenExchangeResponse, error) { ) (*oidc.TokenExchangeResponse, error) {
ctx, span := client.Tracer.Start(ctx, "ExchangeToken")
defer span.End()
if SubjectToken == "" { if SubjectToken == "" {
return nil, errors.New("empty subject_token") return nil, errors.New("empty subject_token")
} }

View file

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

View file

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

134
pkg/crypto/key_test.go Normal file
View file

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

View file

@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "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) { func Sign(object any, signer jose.Signer) (string, error) {

View file

@ -10,6 +10,8 @@ import (
"net/url" "net/url"
"strings" "strings"
"time" "time"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
var DefaultHTTPClient = &http.Client{ var DefaultHTTPClient = &http.Client{
@ -66,7 +68,12 @@ func HttpRequest(client *http.Client, req *http.Request, response any) error {
} }
if resp.StatusCode != http.StatusOK { 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) err = json.Unmarshal(body, response)

View file

@ -1,7 +1,7 @@
package oidc package oidc
import ( import (
"golang.org/x/exp/slog" "log/slog"
) )
const ( const (
@ -48,6 +48,7 @@ const (
ResponseModeQuery ResponseMode = "query" ResponseModeQuery ResponseMode = "query"
ResponseModeFragment ResponseMode = "fragment" ResponseModeFragment ResponseMode = "fragment"
ResponseModeFormPost ResponseMode = "form_post"
// PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages. // PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages.
// An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed // An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed

View file

@ -3,10 +3,10 @@
package oidc package oidc
import ( import (
"log/slog"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/exp/slog"
) )
func TestAuthRequest_LogValue(t *testing.T) { func TestAuthRequest_LogValue(t *testing.T) {

View file

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

View file

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

View file

@ -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)
}

View file

@ -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 is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service.
OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"` OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
// BackChannelLogoutSupported specifies whether the OP supports back-channel logout (https://openid.net/specs/openid-connect-backchannel-1_0.html),
// with true indicating support. If omitted, the default value is false.
BackChannelLogoutSupported bool `json:"backchannel_logout_supported,omitempty"`
// BackChannelLogoutSessionSupported specifies whether the OP can pass a sid (session ID) Claim in the Logout Token to identify the RP session with the OP.
// If supported, the sid Claim is also included in ID Tokens issued by the OP. If omitted, the default value is false.
BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported,omitempty"`
} }
type AuthMethod string type AuthMethod string

View file

@ -1,10 +1,10 @@
package oidc package oidc
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"golang.org/x/exp/slog"
) )
type errorType string type errorType string
@ -28,6 +28,11 @@ const (
SlowDown errorType = "slow_down" SlowDown errorType = "slow_down"
AccessDenied errorType = "access_denied" AccessDenied errorType = "access_denied"
ExpiredToken errorType = "expired_token" ExpiredToken errorType = "expired_token"
// InvalidTarget error is returned by Token Exchange if
// the requested target or audience is invalid.
// [RFC 8693, Section 2.2.2: Error Response](https://www.rfc-editor.org/rfc/rfc8693#section-2.2.2)
InvalidTarget errorType = "invalid_target"
) )
var ( var (
@ -113,6 +118,14 @@ var (
Description: "The \"device_code\" has expired.", Description: "The \"device_code\" has expired.",
} }
} }
// Token exchange error
ErrInvalidTarget = func() *Error {
return &Error{
ErrorType: InvalidTarget,
Description: "The requested audience or target is invalid.",
}
}
) )
type Error struct { type Error struct {
@ -120,7 +133,28 @@ type Error struct {
ErrorType errorType `json:"error" schema:"error"` ErrorType errorType `json:"error" schema:"error"`
Description string `json:"error_description,omitempty" schema:"error_description,omitempty"` Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
State string `json:"state,omitempty" schema:"state,omitempty"` State string `json:"state,omitempty" schema:"state,omitempty"`
SessionState string `json:"session_state,omitempty" schema:"session_state,omitempty"`
redirectDisabled bool `schema:"-"` redirectDisabled bool `schema:"-"`
returnParent bool `schema:"-"`
}
func (e *Error) MarshalJSON() ([]byte, error) {
m := struct {
Error errorType `json:"error"`
ErrorDescription string `json:"error_description,omitempty"`
State string `json:"state,omitempty"`
SessionState string `json:"session_state,omitempty"`
Parent string `json:"parent,omitempty"`
}{
Error: e.ErrorType,
ErrorDescription: e.Description,
State: e.State,
SessionState: e.SessionState,
}
if e.returnParent {
m.Parent = e.Parent.Error()
}
return json.Marshal(m)
} }
func (e *Error) Error() string { func (e *Error) Error() string {
@ -145,7 +179,8 @@ func (e *Error) Is(target error) bool {
} }
return e.ErrorType == t.ErrorType && return e.ErrorType == t.ErrorType &&
(e.Description == t.Description || t.Description == "") && (e.Description == t.Description || t.Description == "") &&
(e.State == t.State || t.State == "") (e.State == t.State || t.State == "") &&
(e.SessionState == t.SessionState || t.SessionState == "")
} }
func (e *Error) WithParent(err error) *Error { func (e *Error) WithParent(err error) *Error {
@ -153,6 +188,18 @@ func (e *Error) WithParent(err error) *Error {
return e 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 { func (e *Error) WithDescription(desc string, args ...any) *Error {
e.Description = fmt.Sprintf(desc, args...) e.Description = fmt.Sprintf(desc, args...)
return e return e
@ -199,6 +246,9 @@ func (e *Error) LogValue() slog.Value {
if e.State != "" { if e.State != "" {
attrs = append(attrs, slog.String("state", 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 { if e.redirectDisabled {
attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled)) attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled))
} }

View file

@ -1,83 +0,0 @@
//go:build go1.20
package oidc
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/exp/slog"
)
func TestError_LogValue(t *testing.T) {
type fields struct {
Parent error
ErrorType errorType
Description string
State string
redirectDisabled bool
}
tests := []struct {
name string
fields fields
want slog.Value
}{
{
name: "parent",
fields: fields{
Parent: io.EOF,
},
want: slog.GroupValue(slog.Any("parent", io.EOF)),
},
{
name: "description",
fields: fields{
Description: "oops",
},
want: slog.GroupValue(slog.String("description", "oops")),
},
{
name: "errorType",
fields: fields{
ErrorType: ExpiredToken,
},
want: slog.GroupValue(slog.String("type", string(ExpiredToken))),
},
{
name: "state",
fields: fields{
State: "123",
},
want: slog.GroupValue(slog.String("state", "123")),
},
{
name: "all fields",
fields: fields{
Parent: io.EOF,
Description: "oops",
ErrorType: ExpiredToken,
State: "123",
},
want: slog.GroupValue(
slog.Any("parent", io.EOF),
slog.String("description", "oops"),
slog.String("type", string(ExpiredToken)),
slog.String("state", "123"),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &Error{
Parent: tt.fields.Parent,
ErrorType: tt.fields.ErrorType,
Description: tt.fields.Description,
State: tt.fields.State,
redirectDisabled: tt.fields.redirectDisabled,
}
got := e.LogValue()
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -1,11 +1,14 @@
package oidc package oidc
import ( import (
"encoding/json"
"errors"
"io" "io"
"log/slog"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/exp/slog" "github.com/stretchr/testify/require"
) )
func TestDefaultToServerError(t *testing.T) { func TestDefaultToServerError(t *testing.T) {
@ -79,3 +82,111 @@ func TestError_LogLevel(t *testing.T) {
}) })
} }
} }
func TestError_LogValue(t *testing.T) {
type fields struct {
Parent error
ErrorType errorType
Description string
State string
redirectDisabled bool
}
tests := []struct {
name string
fields fields
want slog.Value
}{
{
name: "parent",
fields: fields{
Parent: io.EOF,
},
want: slog.GroupValue(slog.Any("parent", io.EOF)),
},
{
name: "description",
fields: fields{
Description: "oops",
},
want: slog.GroupValue(slog.String("description", "oops")),
},
{
name: "errorType",
fields: fields{
ErrorType: ExpiredToken,
},
want: slog.GroupValue(slog.String("type", string(ExpiredToken))),
},
{
name: "state",
fields: fields{
State: "123",
},
want: slog.GroupValue(slog.String("state", "123")),
},
{
name: "all fields",
fields: fields{
Parent: io.EOF,
Description: "oops",
ErrorType: ExpiredToken,
State: "123",
},
want: slog.GroupValue(
slog.Any("parent", io.EOF),
slog.String("description", "oops"),
slog.String("type", string(ExpiredToken)),
slog.String("state", "123"),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &Error{
Parent: tt.fields.Parent,
ErrorType: tt.fields.ErrorType,
Description: tt.fields.Description,
State: tt.fields.State,
redirectDisabled: tt.fields.redirectDisabled,
}
got := e.LogValue()
assert.Equal(t, tt.want, got)
})
}
}
func TestError_MarshalJSON(t *testing.T) {
tests := []struct {
name string
e *Error
want string
}{
{
name: "simple error",
e: ErrAccessDenied(),
want: `{"error":"access_denied","error_description":"The authorization request was denied."}`,
},
{
name: "with description",
e: ErrAccessDenied().WithDescription("oops"),
want: `{"error":"access_denied","error_description":"oops"}`,
},
{
name: "with parent",
e: ErrServerError().WithParent(errors.New("oops")),
want: `{"error":"server_error"}`,
},
{
name: "with return parent",
e: ErrServerError().WithParent(errors.New("oops")).WithReturnParentToClient(true),
want: `{"error":"server_error","parent":"oops"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := json.Marshal(tt.e)
require.NoError(t, err)
assert.JSONEq(t, tt.want, string(got))
})
}
}

View file

@ -16,18 +16,21 @@ type ClientAssertionParams struct {
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2. // https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2.
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims. // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims.
type IntrospectionResponse struct { type IntrospectionResponse struct {
Active bool `json:"active"` Active bool `json:"active"`
Scope SpaceDelimitedArray `json:"scope,omitempty"` Scope SpaceDelimitedArray `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"` ClientID string `json:"client_id,omitempty"`
TokenType string `json:"token_type,omitempty"` TokenType string `json:"token_type,omitempty"`
Expiration Time `json:"exp,omitempty"` Expiration Time `json:"exp,omitempty"`
IssuedAt Time `json:"iat,omitempty"` IssuedAt Time `json:"iat,omitempty"`
NotBefore Time `json:"nbf,omitempty"` AuthTime Time `json:"auth_time,omitempty"`
Subject string `json:"sub,omitempty"` NotBefore Time `json:"nbf,omitempty"`
Audience Audience `json:"aud,omitempty"` Subject string `json:"sub,omitempty"`
Issuer string `json:"iss,omitempty"` Audience Audience `json:"aud,omitempty"`
JWTID string `json:"jti,omitempty"` AuthenticationMethodsReferences []string `json:"amr,omitempty"`
Username string `json:"username,omitempty"` Issuer string `json:"iss,omitempty"`
JWTID string `json:"jti,omitempty"`
Username string `json:"username,omitempty"`
Actor *ActorClaims `json:"act,omitempty"`
UserInfoProfile UserInfoProfile
UserInfoEmail UserInfoEmail
UserInfoPhone UserInfoPhone

View file

@ -6,8 +6,9 @@ import (
"crypto/ed25519" "crypto/ed25519"
"crypto/rsa" "crypto/rsa"
"errors" "errors"
"strings"
jose "github.com/go-jose/go-jose/v3" jose "github.com/go-jose/go-jose/v4"
) )
const ( const (
@ -92,17 +93,17 @@ func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (k
} }
func algToKeyType(key any, alg string) bool { func algToKeyType(key any, alg string) bool {
switch alg[0] { if strings.HasPrefix(alg, "RS") || strings.HasPrefix(alg, "PS") {
case 'R', 'P':
_, ok := key.(*rsa.PublicKey) _, ok := key.(*rsa.PublicKey)
return ok return ok
case 'E': }
if strings.HasPrefix(alg, "ES") {
_, ok := key.(*ecdsa.PublicKey) _, ok := key.(*ecdsa.PublicKey)
return ok return ok
case 'O':
_, ok := key.(*ed25519.PublicKey)
return ok
default:
return false
} }
if alg == string(jose.EdDSA) {
_, ok := key.(ed25519.PublicKey)
return ok
}
return false
} }

View file

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

View file

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

View file

@ -5,11 +5,12 @@ import (
"os" "os"
"time" "time"
jose "github.com/go-jose/go-jose/v3" jose "github.com/go-jose/go-jose/v4"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/muhlemmer/gu" "github.com/muhlemmer/gu"
"github.com/zitadel/oidc/v3/pkg/crypto"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
) )
const ( const (
@ -34,19 +35,20 @@ type Tokens[C IDClaims] struct {
// TokenClaims implements the Claims interface, // TokenClaims implements the Claims interface,
// and can be used to extend larger claim types by embedding. // and can be used to extend larger claim types by embedding.
type TokenClaims struct { type TokenClaims struct {
Issuer string `json:"iss,omitempty"` Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"` Subject string `json:"sub,omitempty"`
Audience Audience `json:"aud,omitempty"` Audience Audience `json:"aud,omitempty"`
Expiration Time `json:"exp,omitempty"` Expiration Time `json:"exp,omitempty"`
IssuedAt Time `json:"iat,omitempty"` IssuedAt Time `json:"iat,omitempty"`
AuthTime Time `json:"auth_time,omitempty"` AuthTime Time `json:"auth_time,omitempty"`
NotBefore Time `json:"nbf,omitempty"` NotBefore Time `json:"nbf,omitempty"`
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
AuthenticationContextClassReference string `json:"acr,omitempty"` AuthenticationContextClassReference string `json:"acr,omitempty"`
AuthenticationMethodsReferences []string `json:"amr,omitempty"` AuthenticationMethodsReferences []string `json:"amr,omitempty"`
AuthorizedParty string `json:"azp,omitempty"` AuthorizedParty string `json:"azp,omitempty"`
ClientID string `json:"client_id,omitempty"` ClientID string `json:"client_id,omitempty"`
JWTID string `json:"jti,omitempty"` JWTID string `json:"jti,omitempty"`
Actor *ActorClaims `json:"act,omitempty"`
// Additional information set by this framework // Additional information set by this framework
SignatureAlg jose.SignatureAlgorithm `json:"-"` SignatureAlg jose.SignatureAlgorithm `json:"-"`
@ -115,6 +117,7 @@ func NewAccessTokenClaims(issuer, subject string, audience []string, expiration
Expiration: FromTime(expiration), Expiration: FromTime(expiration),
IssuedAt: FromTime(now), IssuedAt: FromTime(now),
NotBefore: FromTime(now), NotBefore: FromTime(now),
ClientID: clientID,
JWTID: jwtid, JWTID: jwtid,
}, },
} }
@ -204,13 +207,36 @@ func (i *IDTokenClaims) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims) return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims)
} }
// ActorClaims provides the `act` claims used for impersonation or delegation Token Exchange.
//
// An actor can be nested in case an obtained token is used as actor token to obtain impersonation or delegation.
// This allows creating a chain of actors.
// See [RFC 8693, section 4.1](https://www.rfc-editor.org/rfc/rfc8693#name-act-actor-claim).
type ActorClaims struct {
Actor *ActorClaims `json:"act,omitempty"`
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Claims map[string]any `json:"-"`
}
type acAlias ActorClaims
func (c *ActorClaims) MarshalJSON() ([]byte, error) {
return mergeAndMarshalClaims((*acAlias)(c), c.Claims)
}
func (c *ActorClaims) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*acAlias)(c), &c.Claims)
}
type AccessTokenResponse struct { type AccessTokenResponse struct {
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"` AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"` TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"` RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"` ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"` IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
State string `json:"state,omitempty" schema:"state,omitempty"` State string `json:"state,omitempty" schema:"state,omitempty"`
Scope SpaceDelimitedArray `json:"scope,omitempty" schema:"scope,omitempty"`
} }
type JWTProfileAssertionClaims struct { type JWTProfileAssertionClaims struct {
@ -321,12 +347,12 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
} }
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) { func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) {
privateKey, err := crypto.BytesToPrivateKey(assertion.PrivateKey) privateKey, algorithm, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
if err != nil { if err != nil {
return "", err return "", err
} }
key := jose.SigningKey{ key := jose.SigningKey{
Algorithm: jose.RS256, Algorithm: algorithm,
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
} }
signer, err := jose.NewSigner(key, &jose.SignerOptions{}) signer, err := jose.NewSigner(key, &jose.SignerOptions{})
@ -352,4 +378,45 @@ type TokenExchangeResponse struct {
ExpiresIn uint64 `json:"expires_in,omitempty"` ExpiresIn uint64 `json:"expires_in,omitempty"`
Scopes SpaceDelimitedArray `json:"scope,omitempty"` Scopes SpaceDelimitedArray `json:"scope,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"` RefreshToken string `json:"refresh_token,omitempty"`
// IDToken field allows returning an additional ID token
// if the requested_token_type was Access Token and scope contained openid.
IDToken string `json:"id_token,omitempty"`
}
type LogoutTokenClaims struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Audience Audience `json:"aud,omitempty"`
IssuedAt Time `json:"iat,omitempty"`
Expiration Time `json:"exp,omitempty"`
JWTID string `json:"jti,omitempty"`
Events map[string]any `json:"events,omitempty"`
SessionID string `json:"sid,omitempty"`
Claims map[string]any `json:"-"`
}
type ltcAlias LogoutTokenClaims
func (i *LogoutTokenClaims) MarshalJSON() ([]byte, error) {
return mergeAndMarshalClaims((*ltcAlias)(i), i.Claims)
}
func (i *LogoutTokenClaims) UnmarshalJSON(data []byte) error {
return unmarshalJSONMulti(data, (*ltcAlias)(i), &i.Claims)
}
func NewLogoutTokenClaims(issuer, subject string, audience Audience, expiration time.Time, jwtID, sessionID string, skew time.Duration) *LogoutTokenClaims {
return &LogoutTokenClaims{
Issuer: issuer,
Subject: subject,
Audience: audience,
IssuedAt: FromTime(time.Now().Add(-skew)),
Expiration: FromTime(expiration),
JWTID: jwtID,
Events: map[string]any{
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
},
SessionID: sessionID,
}
} }

View file

@ -3,9 +3,10 @@ package oidc
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"slices"
"time" "time"
jose "github.com/go-jose/go-jose/v3" jose "github.com/go-jose/go-jose/v4"
) )
const ( const (
@ -57,13 +58,7 @@ var AllTokenTypes = []TokenType{
type TokenType string type TokenType string
func (t TokenType) IsSupported() bool { func (t TokenType) IsSupported() bool {
for _, tt := range AllTokenTypes { return slices.Contains(AllTokenTypes, t)
if t == tt {
return true
}
}
return false
} }
type TokenRequest interface { type TokenRequest interface {
@ -77,10 +72,10 @@ type AccessTokenRequest struct {
Code string `schema:"code"` Code string `schema:"code"`
RedirectURI string `schema:"redirect_uri"` RedirectURI string `schema:"redirect_uri"`
ClientID string `schema:"client_id"` ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"` ClientSecret string `schema:"client_secret,omitempty"`
CodeVerifier string `schema:"code_verifier"` CodeVerifier string `schema:"code_verifier,omitempty"`
ClientAssertion string `schema:"client_assertion"` ClientAssertion string `schema:"client_assertion,omitempty"`
ClientAssertionType string `schema:"client_assertion_type"` ClientAssertionType string `schema:"client_assertion_type,omitempty"`
} }
func (a *AccessTokenRequest) GrantType() GrantType { func (a *AccessTokenRequest) GrantType() GrantType {

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
"time" "time"
jose "github.com/go-jose/go-jose/v3" jose "github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
@ -145,6 +145,7 @@ func TestNewAccessTokenClaims(t *testing.T) {
Subject: "hello@me.com", Subject: "hello@me.com",
Audience: Audience{"foo"}, Audience: Audience{"foo"},
Expiration: 12345, Expiration: 12345,
ClientID: "foo",
JWTID: "900", JWTID: "900",
}, },
} }
@ -241,3 +242,39 @@ func TestIDTokenClaims_GetUserInfo(t *testing.T) {
got := idTokenData.GetUserInfo() got := idTokenData.GetUserInfo()
assert.Equal(t, want, got) assert.Equal(t, want, got)
} }
func TestNewLogoutTokenClaims(t *testing.T) {
want := &LogoutTokenClaims{
Issuer: "zitadel",
Subject: "hello@me.com",
Audience: Audience{"foo", "just@me.com"},
Expiration: 12345,
JWTID: "jwtID",
Events: map[string]any{
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
},
SessionID: "sessionID",
Claims: nil,
}
got := NewLogoutTokenClaims(
want.Issuer,
want.Subject,
want.Audience,
want.Expiration.AsTime(),
want.JWTID,
want.SessionID,
1*time.Second,
)
// test if the dynamic timestamp is around now,
// allowing for a delta of 1, just in case we flip on
// either side of a second boundry.
nowMinusSkew := NowTime() - 1
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
// Make equal not fail on dynamic timestamp
got.IssuedAt = 0
assert.Equal(t, want, got)
}

View file

@ -3,12 +3,13 @@ package oidc
import ( import (
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
"time" "time"
jose "github.com/go-jose/go-jose/v3" jose "github.com/go-jose/go-jose/v4"
"github.com/muhlemmer/gu" "github.com/muhlemmer/gu"
"github.com/zitadel/schema" "github.com/zitadel/schema"
"golang.org/x/text/language" "golang.org/x/text/language"
@ -34,6 +35,17 @@ func (a *Audience) UnmarshalJSON(text []byte) error {
return nil return nil
} }
func (a *Audience) MarshalJSON() ([]byte, error) {
len := len(*a)
if len > 1 {
return json.Marshal(*a)
} else if len == 1 {
return json.Marshal((*a)[0])
}
return nil, errors.New("aud is empty")
}
type Display string type Display string
func (d *Display) UnmarshalText(text []byte) error { func (d *Display) UnmarshalText(text []byte) error {
@ -76,8 +88,26 @@ func (l *Locale) MarshalJSON() ([]byte, error) {
return json.Marshal(tag) return json.Marshal(tag)
} }
// UnmarshalJSON implements json.Unmarshaler.
// When [language.ValueError] is encountered, the containing tag will be set
// to an empty value (language "und") and no error will be returned.
// This state can be checked with the `l.Tag().IsRoot()` method.
func (l *Locale) UnmarshalJSON(data []byte) error { func (l *Locale) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &l.tag) if len(data) == 0 || string(data) == "\"\"" {
return nil
}
err := json.Unmarshal(data, &l.tag)
if err == nil {
return nil
}
// catch "well-formed but unknown" errors
var target language.ValueError
if errors.As(err, &target) {
l.tag = language.Tag{}
return nil
}
return err
} }
type Locales []language.Tag type Locales []language.Tag
@ -96,6 +126,14 @@ func ParseLocales(locales []string) Locales {
return out 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. // UnmarshalText implements the [encoding.TextUnmarshaler] interface.
// It decodes an unquoted space seperated string into Locales. // It decodes an unquoted space seperated string into Locales.
// Undefined language tags in the input are ignored and ommited from // Undefined language tags in the input are ignored and ommited from
@ -212,6 +250,9 @@ func NewEncoder() *schema.Encoder {
e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string { e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string {
return value.Interface().(SpaceDelimitedArray).String() return value.Interface().(SpaceDelimitedArray).String()
}) })
e.RegisterEncoder(Locales{}, func(value reflect.Value) string {
return value.Interface().(Locales).String()
})
return e return e
} }

View file

@ -208,20 +208,71 @@ func TestLocale_MarshalJSON(t *testing.T) {
} }
func TestLocale_UnmarshalJSON(t *testing.T) { func TestLocale_UnmarshalJSON(t *testing.T) {
type a struct { type dst struct {
Locale *Locale `json:"locale,omitempty"` Locale *Locale `json:"locale,omitempty"`
} }
want := a{ tests := []struct {
Locale: NewLocale(language.Afrikaans), name string
input string
want dst
wantErr bool
}{
{
name: "value not present",
input: `{}`,
wantErr: false,
want: dst{
Locale: nil,
},
},
{
name: "null",
input: `{"locale": null}`,
wantErr: false,
want: dst{
Locale: nil,
},
},
{
name: "empty, ignored",
input: `{"locale": ""}`,
wantErr: false,
want: dst{
Locale: &Locale{},
},
},
{
name: "afrikaans, ok",
input: `{"locale": "af"}`,
want: dst{
Locale: NewLocale(language.Afrikaans),
},
},
{
name: "gb, ignored",
input: `{"locale": "gb"}`,
want: dst{
Locale: &Locale{},
},
},
{
name: "bad form, error",
input: `{"locale": "g!!!!!"}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got dst
err := json.Unmarshal([]byte(tt.input), &got)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
} }
const input = `{"locale": "af"}`
var got a
require.NoError(t,
json.Unmarshal([]byte(input), &got),
)
assert.Equal(t, want, got)
} }
func TestParseLocales(t *testing.T) { func TestParseLocales(t *testing.T) {

View file

@ -7,12 +7,11 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"slices"
"strings" "strings"
"time" "time"
jose "github.com/go-jose/go-jose/v3" jose "github.com/go-jose/go-jose/v4"
str "github.com/zitadel/oidc/v3/pkg/strings"
) )
type Claims interface { type Claims interface {
@ -41,6 +40,7 @@ type IDClaims interface {
var ( var (
ErrParse = errors.New("parsing of request failed") ErrParse = errors.New("parsing of request failed")
ErrIssuerInvalid = errors.New("issuer does not match") ErrIssuerInvalid = errors.New("issuer does not match")
ErrDiscoveryFailed = errors.New("OpenID Provider Configuration Discovery has failed")
ErrSubjectMissing = errors.New("subject missing") ErrSubjectMissing = errors.New("subject missing")
ErrAudience = errors.New("audience is not valid") ErrAudience = errors.New("audience is not valid")
ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty") ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty")
@ -57,7 +57,7 @@ var (
ErrNonceInvalid = errors.New("nonce does not match") ErrNonceInvalid = errors.New("nonce does not match")
ErrAcrInvalid = errors.New("acr is invalid") ErrAcrInvalid = errors.New("acr is invalid")
ErrAuthTimeNotPresent = errors.New("claim `auth_time` of token is missing") ErrAuthTimeNotPresent = errors.New("claim `auth_time` of token is missing")
ErrAuthTimeToOld = errors.New("auth time of token is to old") ErrAuthTimeToOld = errors.New("auth time of token is too old")
ErrAtHash = errors.New("at_hash does not correspond to access token") ErrAtHash = errors.New("at_hash does not correspond to access token")
) )
@ -83,7 +83,7 @@ type ACRVerifier func(string) error
// if none of the provided values matches the acr claim // if none of the provided values matches the acr claim
func DefaultACRVerifier(possibleValues []string) ACRVerifier { func DefaultACRVerifier(possibleValues []string) ACRVerifier {
return func(acr string) error { return func(acr string) error {
if !str.Contains(possibleValues, acr) { if !slices.Contains(possibleValues, acr) {
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr) return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
} }
return nil return nil
@ -122,7 +122,7 @@ func CheckIssuer(claims Claims, issuer string) error {
} }
func CheckAudience(claims Claims, clientID 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) 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 { 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 err != nil {
if strings.HasPrefix(err.Error(), "go-jose/go-jose: unexpected signature algorithm") {
// TODO(v4): we should wrap errors instead of returning static ones.
// This is a workaround so we keep returning the same error for now.
return ErrSignatureUnsupportedAlg
}
return ErrParse return ErrParse
} }
if len(jws.Signatures) == 0 { if len(jws.Signatures) == 0 {
@ -159,12 +164,6 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl
return ErrSignatureMultiple return ErrSignatureMultiple
} }
sig := jws.Signatures[0] sig := jws.Signatures[0]
if len(supportedSigAlgs) == 0 {
supportedSigAlgs = []string{"RS256"}
}
if !str.Contains(supportedSigAlgs, sig.Header.Algorithm) {
return fmt.Errorf("%w: id token signed with unsupported algorithm, expected %q got %q", ErrSignatureUnsupportedAlg, supportedSigAlgs, sig.Header.Algorithm)
}
signedPayload, err := set.VerifySignature(ctx, jws) signedPayload, err := set.VerifySignature(ctx, jws)
if err != nil { if err != nil {
@ -180,6 +179,18 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl
return nil return nil
} }
// TODO(v4): Use the new jose.SignatureAlgorithm type directly, instead of string.
func toJoseSignatureAlgorithms(algorithms []string) []jose.SignatureAlgorithm {
out := make([]jose.SignatureAlgorithm, len(algorithms))
for i := range algorithms {
out[i] = jose.SignatureAlgorithm(algorithms[i])
}
if len(out) == 0 {
out = append(out, jose.RS256, jose.ES256, jose.PS256)
}
return out
}
func CheckExpiration(claims Claims, offset time.Duration) error { func CheckExpiration(claims Claims, offset time.Duration) error {
expiration := claims.GetExpiration() expiration := claims.GetExpiration()
if !time.Now().Add(offset).Before(expiration) { if !time.Now().Add(offset).Before(expiration) {

View file

@ -5,10 +5,10 @@ import (
"encoding/json" "encoding/json"
"testing" "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/assert"
"github.com/stretchr/testify/require" "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) { func TestParseToken(t *testing.T) {

View file

@ -1,20 +1,23 @@
package op package op
import ( import (
"bytes"
"context" "context"
_ "embed"
"errors" "errors"
"fmt" "fmt"
"html/template"
"log/slog"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"path" "slices"
"strings" "strings"
"time" "time"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
str "github.com/zitadel/oidc/v3/pkg/strings" "github.com/bmatcuk/doublestar/v4"
"golang.org/x/exp/slog"
) )
type AuthRequest interface { type AuthRequest interface {
@ -35,6 +38,13 @@ type AuthRequest interface {
Done() bool Done() bool
} }
// AuthRequestSessionState should be implemented if [OpenID Connect Session Management](https://openid.net/specs/openid-connect-session-1_0.html) is supported
type AuthRequestSessionState interface {
// GetSessionState returns session_state.
// session_state is related to OpenID Connect Session Management.
GetSessionState() string
}
type Authorizer interface { type Authorizer interface {
Storage() Storage Storage() Storage
Decoder() httphelper.Decoder Decoder() httphelper.Decoder
@ -52,13 +62,19 @@ type AuthorizeValidator interface {
ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, *IDTokenHintVerifier) (string, error) ValidateAuthRequest(context.Context, *oidc.AuthRequest, Storage, *IDTokenHintVerifier) (string, error)
} }
type CodeResponseType struct {
Code string `schema:"code"`
State string `schema:"state,omitempty"`
SessionState string `schema:"session_state,omitempty"`
}
func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) { func authorizeHandler(authorizer Authorizer) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
Authorize(w, r, authorizer) 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) { return func(w http.ResponseWriter, r *http.Request) {
AuthorizeCallback(w, r, authorizer) AuthorizeCallback(w, r, authorizer)
} }
@ -67,30 +83,41 @@ func authorizeCallbackHandler(authorizer Authorizer) func(http.ResponseWriter, *
// Authorize handles the authorization request, including // Authorize handles the authorization request, including
// parsing, validating, storing and finally redirecting to the login handler // parsing, validating, storing and finally redirecting to the login handler
func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
ctx, span := tracer.Start(r.Context(), "Authorize")
r = r.WithContext(ctx)
defer span.End()
authReq, err := ParseAuthorizeRequest(r, authorizer.Decoder()) authReq, err := ParseAuthorizeRequest(r, authorizer.Decoder())
if err != nil { if err != nil {
AuthRequestError(w, r, nil, err, authorizer) AuthRequestError(w, r, nil, err, authorizer)
return return
} }
ctx := r.Context()
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() { if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx)) err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
if err != nil { if err != nil {
AuthRequestError(w, r, authReq, err, authorizer) AuthRequestError(w, r, nil, err, authorizer)
return return
} }
} }
if authReq.ClientID == "" { 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 return
} }
if authReq.RedirectURI == "" { 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 return
} }
validation := ValidateAuthRequest
if validater, ok := authorizer.(AuthorizeValidator); ok { var client Client
validation = validater.ValidateAuthRequest 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)) userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx))
if err != nil { 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) AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer)
return 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) RedirectToLogin(req.GetID(), client, w, r)
} }
@ -138,20 +160,20 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage
} }
if requestObject.ClientID != "" && requestObject.ClientID != authReq.ClientID { if requestObject.ClientID != "" && requestObject.ClientID != authReq.ClientID {
return oidc.ErrInvalidRequest() return oidc.ErrInvalidRequest().WithDescription("missing or wrong client id in request")
} }
if requestObject.ResponseType != "" && requestObject.ResponseType != authReq.ResponseType { if requestObject.ResponseType != "" && requestObject.ResponseType != authReq.ResponseType {
return oidc.ErrInvalidRequest() return oidc.ErrInvalidRequest().WithDescription("missing or wrong response type in request")
} }
if requestObject.Issuer != requestObject.ClientID { if requestObject.Issuer != requestObject.ClientID {
return oidc.ErrInvalidRequest() 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() return oidc.ErrInvalidRequest().WithDescription("issuer missing in audience")
} }
keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer} keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer}
if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil { if err = oidc.CheckSignature(ctx, authReq.RequestParam, payload, requestObject, nil, keySet); err != nil {
return err return oidc.ErrInvalidRequest().WithParent(err).WithDescription(err.Error())
} }
CopyRequestObjectToAuthRequest(authReq, requestObject) CopyRequestObjectToAuthRequest(authReq, requestObject)
return nil return nil
@ -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 // CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request
// and clears the `RequestParam` of the auth request // and clears the `RequestParam` of the auth request
func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) { func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) {
if 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 authReq.Scopes = requestObject.Scopes
} }
if requestObject.RedirectURI != "" { if requestObject.RedirectURI != "" {
@ -205,23 +227,37 @@ func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oi
authReq.RequestParam = "" authReq.RequestParam = ""
} }
// ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed // ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed.
//
// 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) { func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) {
ctx, span := tracer.Start(ctx, "ValidateAuthRequest")
defer span.End()
client, err := storage.GetClientByClientID(ctx, authReq.ClientID)
if err != nil {
return "", oidc.ErrInvalidRequestRedirectURI().WithDescription("unable to retrieve client by id").WithParent(err)
}
return ValidateAuthRequestClient(ctx, authReq, client, verifier)
}
// ValidateAuthRequestClient validates the Auth request against the passed client.
// If id_token_hint is part of the request, the subject of the token is returned.
func ValidateAuthRequestClient(ctx context.Context, authReq *oidc.AuthRequest, client Client, verifier *IDTokenHintVerifier) (sub string, err error) {
ctx, span := tracer.Start(ctx, "ValidateAuthRequestClient")
defer span.End()
if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil {
return "", err
}
authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge) authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge)
if err != nil { if err != nil {
return "", err return "", err
} }
client, err := storage.GetClientByClientID(ctx, authReq.ClientID)
if err != nil {
return "", oidc.DefaultToServerError(err, "unable to retrieve client by id")
}
authReq.Scopes, err = ValidateAuthReqScopes(client, authReq.Scopes) authReq.Scopes, err = ValidateAuthReqScopes(client, authReq.Scopes)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil {
return "", err
}
if err := ValidateAuthReqResponseType(client, authReq.ResponseType); err != nil { if err := ValidateAuthReqResponseType(client, authReq.ResponseType); err != nil {
return "", err return "", err
} }
@ -241,49 +277,35 @@ func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error)
return maxAge, nil 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) { func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) {
if len(scopes) == 0 { if len(scopes) == 0 {
return nil, oidc.ErrInvalidRequest(). return nil, oidc.ErrInvalidRequest().
WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " + WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " +
"If you have any questions, you may contact the administrator of the application.") "If you have any questions, you may contact the administrator of the application.")
} }
openID := false scopes = slices.DeleteFunc(scopes, func(scope string) bool {
for i := len(scopes) - 1; i >= 0; i-- { return !(scope == oidc.ScopeOpenID ||
scope := scopes[i] scope == oidc.ScopeProfile ||
if scope == oidc.ScopeOpenID {
openID = true
continue
}
if !(scope == oidc.ScopeProfile ||
scope == oidc.ScopeEmail || scope == oidc.ScopeEmail ||
scope == oidc.ScopePhone || scope == oidc.ScopePhone ||
scope == oidc.ScopeAddress || scope == oidc.ScopeAddress ||
scope == oidc.ScopeOfflineAccess) && scope == oidc.ScopeOfflineAccess) &&
!client.IsScopeAllowed(scope) { !client.IsScopeAllowed(scope)
scopes[i] = scopes[len(scopes)-1] })
scopes[len(scopes)-1] = ""
scopes = scopes[:len(scopes)-1]
}
}
if !openID {
return nil, oidc.ErrInvalidScope().WithDescription("The scope openid is missing in your request. " +
"Please ensure the scope openid is added to the request. " +
"If you have any questions, you may contact the administrator of the application.")
}
return scopes, nil return scopes, nil
} }
// checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores // checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores
// other factors. // other factors.
func checkURIAgainstRedirects(client Client, uri string) error { func checkURIAgainstRedirects(client Client, uri string) error {
if str.Contains(client.RedirectURIs(), uri) { if slices.Contains(client.RedirectURIs(), uri) {
return nil return nil
} }
if globClient, ok := client.(HasRedirectGlobs); ok { if globClient, ok := client.(HasRedirectGlobs); ok {
for _, uriGlob := range globClient.RedirectURIGlobs() { for _, uriGlob := range globClient.RedirectURIGlobs() {
isMatch, err := path.Match(uriGlob, uri) isMatch, err := doublestar.Match(uriGlob, uri)
if err != nil { if err != nil {
return oidc.ErrServerError().WithParent(err) return oidc.ErrServerError().WithParent(err)
} }
@ -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. " + return oidc.ErrInvalidRequestRedirectURI().WithDescription("The redirect_uri is missing in the request. " +
"Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.") "Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.")
} }
if client.ApplicationType() == ApplicationTypeNative {
return validateAuthReqRedirectURINative(client, uri)
}
if strings.HasPrefix(uri, "https://") { if strings.HasPrefix(uri, "https://") {
return checkURIAgainstRedirects(client, uri) return checkURIAgainstRedirects(client, uri)
} }
if client.ApplicationType() == ApplicationTypeNative {
return validateAuthReqRedirectURINative(client, uri, responseType)
}
if err := checkURIAgainstRedirects(client, uri); err != nil { if err := checkURIAgainstRedirects(client, uri); err != nil {
return err return err
} }
@ -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 // 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) 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 err := checkURIAgainstRedirects(client, uri); err == nil {
if client.DevMode() { if client.DevMode() {
return nil 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 { if isLoopback || isCustomSchema {
return nil return nil
} }
@ -359,16 +384,16 @@ func equalURI(url1, url2 *url.URL) bool {
return url1.Path == url2.Path && url1.RawQuery == url2.RawQuery return url1.Path == url2.Path && url1.RawQuery == url2.RawQuery
} }
func HTTPLoopbackOrLocalhost(rawurl string) (*url.URL, bool) { func HTTPLoopbackOrLocalhost(rawURL string) (*url.URL, bool) {
parsedURL, err := url.Parse(rawurl) parsedURL, err := url.Parse(rawURL)
if err != nil { if err != nil {
return nil, false return nil, false
} }
if parsedURL.Scheme != "http" { if parsedURL.Scheme == "http" || parsedURL.Scheme == "https" {
return nil, false hostName := parsedURL.Hostname()
return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback()
} }
hostName := parsedURL.Hostname() return nil, false
return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback()
} }
// ValidateAuthReqResponseType validates the passed response_type to the registered response types // ValidateAuthReqResponseType validates the passed response_type to the registered response types
@ -391,9 +416,9 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie
return "", nil return "", nil
} }
claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, idTokenHint, verifier) claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, idTokenHint, verifier)
if err != nil { if err != nil && !errors.As(err, &IDTokenHintExpiredError{}) {
return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " + return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " +
"If you have any questions, you may contact the administrator of the application.") "If you have any questions, you may contact the administrator of the application.").WithParent(err)
} }
return claims.GetSubject(), nil return claims.GetSubject(), nil
} }
@ -406,6 +431,10 @@ func RedirectToLogin(authReqID string, client Client, w http.ResponseWriter, r *
// AuthorizeCallback handles the callback after authentication in the Login UI // AuthorizeCallback handles the callback after authentication in the Login UI
func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) { func AuthorizeCallback(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
ctx, span := tracer.Start(r.Context(), "AuthorizeCallback")
r = r.WithContext(ctx)
defer span.End()
id, err := ParseAuthorizeCallbackRequest(r) id, err := ParseAuthorizeCallbackRequest(r)
if err != nil { if err != nil {
AuthRequestError(w, r, nil, err, authorizer) 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) // AuthResponse creates the successful authentication response (either code or tokens)
func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) { func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "AuthResponse")
r = r.WithContext(ctx)
defer span.End()
client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID()) client, err := authorizer.Storage().GetClientByClientID(r.Context(), authReq.GetClientID())
if err != nil { if err != nil {
AuthRequestError(w, r, authReq, err, authorizer) AuthRequestError(w, r, authReq, err, authorizer)
@ -450,26 +483,70 @@ func AuthResponse(authReq AuthRequest, authorizer Authorizer, w http.ResponseWri
AuthResponseToken(w, r, authReq, authorizer, client) 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) { 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 { if err != nil {
AuthRequestError(w, r, authReq, err, authorizer) AuthRequestError(w, r, authReq, err, authorizer)
return
} }
codeResponse := struct { }
Code string `schema:"code"`
State string `schema:"state,omitempty"` // handleFormPostResponse processes the authentication response using form post method
}{ func handleFormPostResponse(w http.ResponseWriter, r *http.Request, authReq AuthRequest, authorizer Authorizer) error {
Code: code, codeResponse, err := BuildAuthResponseCodeResponsePayload(r.Context(), authReq, authorizer)
State: authReq.GetState(),
}
callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder())
if err != nil { if err != nil {
AuthRequestError(w, r, authReq, err, authorizer) return err
return
} }
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 // 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) AuthRequestError(w, r, authReq, err, authorizer)
return return
} }
if authReq.GetResponseMode() == oidc.ResponseModeFormPost {
err := AuthResponseFormPost(w, authReq.GetRedirectURI(), resp, authorizer.Encoder())
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer)
return
}
return
}
callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), resp, authorizer.Encoder()) callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), resp, authorizer.Encoder())
if err != nil { if err != nil {
AuthRequestError(w, r, authReq, err, authorizer) AuthRequestError(w, r, authReq, err, authorizer)
@ -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 // CreateAuthRequestCode creates and stores a code for the auth code response
func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Storage, crypto Crypto) (string, error) { func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Storage, crypto Crypto) (string, error) {
ctx, span := tracer.Start(ctx, "CreateAuthRequestCode")
defer span.End()
code, err := BuildAuthRequestCode(authReq, crypto) code, err := BuildAuthRequestCode(authReq, crypto)
if err != nil { if err != nil {
return "", err return "", err
@ -535,6 +626,43 @@ func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, respons
return mergeQueryParams(uri, params), nil return mergeQueryParams(uri, params), nil
} }
//go:embed form_post.html.tmpl
var formPostHtmlTemplate string
var formPostTmpl = template.Must(template.New("form_post").Parse(formPostHtmlTemplate))
// AuthResponseFormPost responds a html page that automatically submits the form which contains the auth response parameters
func AuthResponseFormPost(res http.ResponseWriter, redirectURI string, response any, encoder httphelper.Encoder) error {
values := make(map[string][]string)
err := encoder.Encode(response, values)
if err != nil {
return oidc.ErrServerError().WithParent(err)
}
params := &struct {
RedirectURI string
Params any
}{
RedirectURI: redirectURI,
Params: values,
}
var buf bytes.Buffer
err = formPostTmpl.Execute(&buf, params)
if err != nil {
return oidc.ErrServerError().WithParent(err)
}
res.Header().Set("Cache-Control", "no-store")
res.WriteHeader(http.StatusOK)
_, err = buf.WriteTo(res)
if err != nil {
return oidc.ErrServerError().WithParent(err)
}
return nil
}
func setFragment(uri *url.URL, params url.Values) string { func setFragment(uri *url.URL, params url.Values) string {
uri.Fragment = params.Encode() uri.Fragment = params.Encode()
return uri.String() return uri.String()

View file

@ -4,23 +4,23 @@ import (
"context" "context"
"errors" "errors"
"io" "io"
"log/slog"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"reflect" "reflect"
"testing" "testing"
"git.christmann.info/LARA/zitadel-oidc/v3/example/server/storage"
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/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zitadel/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" "github.com/zitadel/schema"
"golang.org/x/exp/slog"
) )
func TestAuthorize(t *testing.T) { func TestAuthorize(t *testing.T) {
@ -137,11 +137,6 @@ func TestValidateAuthRequest(t *testing.T) {
args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil}, args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil},
oidc.ErrInvalidRequest(), oidc.ErrInvalidRequest(),
}, },
{
"scope openid missing fails",
args{&oidc.AuthRequest{Scopes: []string{"profile"}}, mock.NewMockStorageExpectValidClientID(t), nil},
oidc.ErrInvalidScope(),
},
{ {
"response_type missing fails", "response_type missing fails",
args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil}, args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil},
@ -287,16 +282,6 @@ func TestValidateAuthReqScopes(t *testing.T) {
err: true, err: true,
}, },
}, },
{
"scope openid missing fails",
args{
mock.NewClientExpectAny(t, op.ApplicationTypeWeb),
[]string{"email"},
},
res{
err: true,
},
},
{ {
"scope ok", "scope ok",
args{ args{
@ -448,6 +433,24 @@ func TestValidateAuthReqRedirectURI(t *testing.T) {
}, },
false, false,
}, },
{
"code flow registered https loopback v4 native ok",
args{
"https://127.0.0.1:4200/callback",
mock.NewClientWithConfig(t, []string{"https://127.0.0.1/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow registered https loopback v6 native ok",
args{
"https://[::1]:4200/callback",
mock.NewClientWithConfig(t, []string{"https://[::1]/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{ {
"code flow unregistered http native fails", "code flow unregistered http native fails",
args{ args{
@ -583,6 +586,60 @@ func TestValidateAuthReqRedirectURI(t *testing.T) {
}, },
false, false,
}, },
{
"code flow dev mode has redirect globs regular ok",
args{
"http://registered.com/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://registered.com/*"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode has redirect globs wildcard ok",
args{
"http://registered.com/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://registered.com/*"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode has redirect globs double star ok",
args{
"http://registered.com/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://**/*"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode has redirect globs double star ok",
args{
"http://registered.com/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://**/*"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode has redirect globs IPv6 ok",
args{
"http://[::1]:80/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://\\[::1\\]:80/*"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow dev mode has redirect globs bad pattern",
args{
"http://registered.com/callback",
mock.NewHasRedirectGlobsWithConfig(t, []string{"http://**/\\"}, op.ApplicationTypeUserAgent, nil, true),
oidc.ResponseTypeCode,
},
true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -973,9 +1030,10 @@ func TestAuthResponseCode(t *testing.T) {
authorizer func(*testing.T) op.Authorizer authorizer func(*testing.T) op.Authorizer
} }
type res struct { type res struct {
wantCode int wantCode int
wantLocationHeader string wantLocationHeader string
wantBody string wantCacheControlHeader string
wantBody string
} }
tests := []struct { tests := []struct {
name string name string
@ -1017,7 +1075,7 @@ func TestAuthResponseCode(t *testing.T) {
authorizer: func(t *testing.T) op.Authorizer { authorizer: func(t *testing.T) op.Authorizer {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
storage := mock.NewMockStorage(ctrl) storage := mock.NewMockStorage(ctrl)
storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1") storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1")
authorizer := mock.NewMockAuthorizer(ctrl) authorizer := mock.NewMockAuthorizer(ctrl)
authorizer.EXPECT().Storage().Return(storage) authorizer.EXPECT().Storage().Return(storage)
@ -1032,6 +1090,34 @@ func TestAuthResponseCode(t *testing.T) {
wantBody: "", 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 name: "success without state", // reproduce issue #415
args: args{ args: args{
@ -1042,7 +1128,7 @@ func TestAuthResponseCode(t *testing.T) {
authorizer: func(t *testing.T) op.Authorizer { authorizer: func(t *testing.T) op.Authorizer {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
storage := mock.NewMockStorage(ctrl) storage := mock.NewMockStorage(ctrl)
storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1") storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1")
authorizer := mock.NewMockAuthorizer(ctrl) authorizer := mock.NewMockAuthorizer(ctrl)
authorizer.EXPECT().Storage().Return(storage) authorizer.EXPECT().Storage().Return(storage)
@ -1057,6 +1143,33 @@ func TestAuthResponseCode(t *testing.T) {
wantBody: "", 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -1067,6 +1180,7 @@ func TestAuthResponseCode(t *testing.T) {
defer resp.Body.Close() defer resp.Body.Close()
assert.Equal(t, tt.res.wantCode, resp.StatusCode) assert.Equal(t, tt.res.wantCode, resp.StatusCode)
assert.Equal(t, tt.res.wantLocationHeader, resp.Header.Get("Location")) 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) body, err := io.ReadAll(resp.Body)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tt.res.wantBody, string(body)) assert.Equal(t, tt.res.wantBody, string(body))
@ -1111,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) { func TestValidateAuthReqIDTokenHint(t *testing.T) {
token, _ := tu.ValidIDToken() token, _ := tu.ValidIDToken()
tests := []struct { tests := []struct {
@ -1141,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)
}
}
})
}
}

View file

@ -7,8 +7,8 @@ import (
"net/url" "net/url"
"time" "time"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
//go:generate go get github.com/dmarkham/enumer //go:generate go get github.com/dmarkham/enumer
@ -63,6 +63,7 @@ type Client interface {
// such as DevMode for the client being enabled. // such as DevMode for the client being enabled.
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
type HasRedirectGlobs interface { type HasRedirectGlobs interface {
Client
RedirectURIGlobs() []string RedirectURIGlobs() []string
PostLogoutRedirectURIGlobs() []string PostLogoutRedirectURIGlobs() []string
} }
@ -91,6 +92,9 @@ type ClientJWTProfile interface {
} }
func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) { func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) {
ctx, span := tracer.Start(ctx, "ClientJWTAuth")
defer span.End()
if ca.ClientAssertion == "" { if ca.ClientAssertion == "" {
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials) return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
} }
@ -103,6 +107,10 @@ func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier
} }
func ClientBasicAuth(r *http.Request, storage Storage) (clientID string, err error) { func ClientBasicAuth(r *http.Request, storage Storage) (clientID string, err error) {
ctx, span := tracer.Start(r.Context(), "ClientBasicAuth")
r = r.WithContext(ctx)
defer span.End()
clientID, clientSecret, ok := r.BasicAuth() clientID, clientSecret, ok := r.BasicAuth()
if !ok { if !ok {
return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials) return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
@ -150,6 +158,10 @@ func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, au
return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err) return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err)
} }
ctx, span := tracer.Start(r.Context(), "ClientIDFromRequest")
r = r.WithContext(ctx)
defer span.End()
data := new(clientData) data := new(clientData)
if err = p.Decoder().Decode(data, r.Form); err != nil { if err = p.Decoder().Decode(data, r.Form); err != nil {
return "", false, err return "", false, err
@ -170,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` // if the client did not send a Basic Auth Header, ignore the `ErrNoClientCredentials`
// but return other errors immediately // but return other errors immediately
if err != nil && !errors.Is(err, ErrNoClientCredentials) { if !errors.Is(err, ErrNoClientCredentials) {
return "", false, err return "", false, err
} }

View file

@ -10,13 +10,13 @@ import (
"strings" "strings"
"testing" "testing"
httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op/mock"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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" "github.com/zitadel/schema"
) )
@ -108,7 +108,7 @@ func TestClientBasicAuth(t *testing.T) {
}, },
storage: func() op.Storage { storage: func() op.Storage {
s := mock.NewMockStorage(gomock.NewController(t)) s := mock.NewMockStorage(gomock.NewController(t))
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "wrong").Return(errWrong) s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "wrong").Return(errWrong)
return s return s
}(), }(),
wantErr: errWrong, wantErr: errWrong,
@ -121,7 +121,7 @@ func TestClientBasicAuth(t *testing.T) {
}, },
storage: func() op.Storage { storage: func() op.Storage {
s := mock.NewMockStorage(gomock.NewController(t)) s := mock.NewMockStorage(gomock.NewController(t))
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil) s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "bar").Return(nil)
return s return s
}(), }(),
wantClientID: "foo", wantClientID: "foo",
@ -207,7 +207,7 @@ func TestClientIDFromRequest(t *testing.T) {
p: testClientProvider{ p: testClientProvider{
storage: func() op.Storage { storage: func() op.Storage {
s := mock.NewMockStorage(gomock.NewController(t)) s := mock.NewMockStorage(gomock.NewController(t))
s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil) s.EXPECT().AuthorizeClientIDSecret(gomock.Any(), "foo", "bar").Return(nil)
return s return s
}(), }(),
}, },

View file

@ -30,6 +30,7 @@ type Configuration interface {
EndSessionEndpoint() *Endpoint EndSessionEndpoint() *Endpoint
KeysEndpoint() *Endpoint KeysEndpoint() *Endpoint
DeviceAuthorizationEndpoint() *Endpoint DeviceAuthorizationEndpoint() *Endpoint
CheckSessionIframe() *Endpoint
AuthMethodPostSupported() bool AuthMethodPostSupported() bool
CodeMethodS256Supported() bool CodeMethodS256Supported() bool
@ -49,6 +50,9 @@ type Configuration interface {
SupportedUILocales() []language.Tag SupportedUILocales() []language.Tag
DeviceAuthorization() DeviceAuthorizationConfig DeviceAuthorization() DeviceAuthorizationConfig
BackChannelLogoutSupported() bool
BackChannelLogoutSessionSupported() bool
} }
type IssuerFromRequest func(r *http.Request) string type IssuerFromRequest func(r *http.Request) string

View file

@ -1,7 +1,7 @@
package op package op
import ( import (
"github.com/zitadel/oidc/v3/pkg/crypto" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/crypto"
) )
type Crypto interface { type Crypto interface {

View file

@ -9,11 +9,12 @@ import (
"math/big" "math/big"
"net/http" "net/http"
"net/url" "net/url"
"slices"
"strings" "strings"
"time" "time"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
type DeviceAuthorizationConfig struct { type DeviceAuthorizationConfig struct {
@ -63,6 +64,10 @@ func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *htt
} }
func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error { 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) req, err := ParseDeviceCodeRequest(r, o)
if err != nil { if err != nil {
return err return err
@ -77,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) { 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()) storage, err := assertDeviceStorage(o.Storage())
if err != nil { if err != nil {
return nil, err return nil, err
} }
config := o.DeviceAuthorization() config := o.DeviceAuthorization()
deviceCode, err := NewDeviceCode(RecommendedDeviceCodeBytes) deviceCode, _ := NewDeviceCode(RecommendedDeviceCodeBytes)
if err != nil {
return nil, NewStatusError(err, http.StatusInternalServerError)
}
userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval) userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval)
if err != nil { if err != nil {
return nil, NewStatusError(err, http.StatusInternalServerError) return nil, NewStatusError(err, http.StatusInternalServerError)
@ -126,6 +131,10 @@ func createDeviceAuthorization(ctx context.Context, req *oidc.DeviceAuthorizatio
} }
func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) { 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) clientID, _, err := ClientIDFromRequest(r, o)
if err != nil { if err != nil {
return nil, err return nil, err
@ -151,11 +160,14 @@ func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuth
// results in a 22 character base64 encoded string. // results in a 22 character base64 encoded string.
const RecommendedDeviceCodeBytes = 16 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) { func NewDeviceCode(nBytes int) (string, error) {
bytes := make([]byte, nBytes) bytes := make([]byte, nBytes)
if _, err := rand.Read(bytes); err != nil { rand.Read(bytes)
return "", fmt.Errorf("%w getting entropy for device code", err)
}
return base64.RawURLEncoding.EncodeToString(bytes), nil return base64.RawURLEncoding.EncodeToString(bytes), nil
} }
@ -185,24 +197,6 @@ func NewUserCode(charSet []rune, charAmount, dashInterval int) (string, error) {
return buf.String(), nil return buf.String(), nil
} }
type deviceAccessTokenRequest struct {
subject string
audience []string
scopes []string
}
func (r *deviceAccessTokenRequest) GetSubject() string {
return r.subject
}
func (r *deviceAccessTokenRequest) GetAudience() []string {
return r.audience
}
func (r *deviceAccessTokenRequest) GetScopes() []string {
return r.scopes
}
func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
ctx, span := tracer.Start(r.Context(), "DeviceAccessToken") ctx, span := tracer.Start(r.Context(), "DeviceAccessToken")
defer span.End() defer span.End()
@ -229,7 +223,7 @@ func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchang
if err != nil { if err != nil {
return err return err
} }
state, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger) tokenRequest, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger)
if err != nil { if err != nil {
return err return err
} }
@ -243,11 +237,6 @@ func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchang
WithDescription("confidential client requires authentication") WithDescription("confidential client requires authentication")
} }
tokenRequest := &deviceAccessTokenRequest{
subject: state.Subject,
audience: []string{clientID},
scopes: state.Scopes,
}
resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client) resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client)
if err != nil { if err != nil {
return err return err
@ -265,7 +254,54 @@ func ParseDeviceAccessTokenRequest(r *http.Request, exchanger Exchanger) (*oidc.
return req, nil return req, nil
} }
// DeviceAuthorizationState describes the current state of
// the device authorization flow.
// It implements the [IDTokenRequest] interface.
type DeviceAuthorizationState struct {
ClientID string
Audience []string
Scopes []string
Expires time.Time // The time after we consider the authorization request timed-out
Done bool // The user authenticated and approved the authorization request
Denied bool // The user authenticated and denied the authorization request
// The following fields are populated after Done == true
Subject string
AMR []string
AuthTime time.Time
}
func (r *DeviceAuthorizationState) GetAMR() []string {
return r.AMR
}
func (r *DeviceAuthorizationState) GetAudience() []string {
if !slices.Contains(r.Audience, r.ClientID) {
r.Audience = append(r.Audience, r.ClientID)
}
return r.Audience
}
func (r *DeviceAuthorizationState) GetAuthTime() time.Time {
return r.AuthTime
}
func (r *DeviceAuthorizationState) GetClientID() string {
return r.ClientID
}
func (r *DeviceAuthorizationState) GetScopes() []string {
return r.Scopes
}
func (r *DeviceAuthorizationState) GetSubject() string {
return r.Subject
}
func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) { 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()) storage, err := assertDeviceStorage(exchanger.Storage())
if err != nil { if err != nil {
return nil, err return nil, err
@ -291,15 +327,33 @@ func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode str
} }
func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) { func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) {
/* TODO(v4):
Change the TokenRequest argument type to *DeviceAuthorizationState.
Breaking change that can not be done for v3.
*/
ctx, span := tracer.Start(ctx, "CreateDeviceTokenResponse")
defer span.End()
accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, client.AccessTokenType(), creator, client, "") accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, client.AccessTokenType(), creator, client, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &oidc.AccessTokenResponse{ response := &oidc.AccessTokenResponse{
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: refreshToken, RefreshToken: refreshToken,
TokenType: oidc.BearerToken, TokenType: oidc.BearerToken,
ExpiresIn: uint64(validity.Seconds()), ExpiresIn: uint64(validity.Seconds()),
}, nil Scope: tokenRequest.GetScopes(),
}
// TODO(v4): remove type assertion
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
}
}
return response, nil
} }

View file

@ -13,12 +13,12 @@ import (
"testing" "testing"
"time" "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/muhlemmer/gu"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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) { func Test_deviceAuthorizationHandler(t *testing.T) {
@ -145,21 +145,11 @@ func runWithRandReader(r io.Reader, f func()) {
} }
func TestNewDeviceCode(t *testing.T) { func TestNewDeviceCode(t *testing.T) {
t.Run("reader error", func(t *testing.T) { for i := 1; i <= 32; i++ {
runWithRandReader(errReader{}, func() { got, err := op.NewDeviceCode(i)
_, err := op.NewDeviceCode(16) require.NoError(t, err)
require.Error(t, err) assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i))
}) }
})
t.Run("different lengths, rand reader", func(t *testing.T) {
for i := 1; i <= 32; i++ {
got, err := op.NewDeviceCode(i)
require.NoError(t, err)
assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i))
}
})
} }
func TestNewUserCode(t *testing.T) { func TestNewUserCode(t *testing.T) {
@ -453,3 +443,96 @@ func TestCheckDeviceAuthorizationState(t *testing.T) {
}) })
} }
} }
func TestCreateDeviceTokenResponse(t *testing.T) {
tests := []struct {
name string
tokenRequest op.TokenRequest
wantAccessToken bool
wantRefreshToken bool
wantIDToken bool
wantErr bool
}{
{
name: "access token",
tokenRequest: &op.DeviceAuthorizationState{
ClientID: "client1",
Subject: "id1",
AMR: []string{"password"},
AuthTime: time.Now(),
},
wantAccessToken: true,
},
{
name: "access and refresh tokens",
tokenRequest: &op.DeviceAuthorizationState{
ClientID: "client1",
Subject: "id1",
AMR: []string{"password"},
AuthTime: time.Now(),
Scopes: []string{oidc.ScopeOfflineAccess},
},
wantAccessToken: true,
wantRefreshToken: true,
},
{
name: "access and id token",
tokenRequest: &op.DeviceAuthorizationState{
ClientID: "client1",
Subject: "id1",
AMR: []string{"password"},
AuthTime: time.Now(),
Scopes: []string{oidc.ScopeOpenID},
},
wantAccessToken: true,
wantIDToken: true,
},
{
name: "access, refresh and id token",
tokenRequest: &op.DeviceAuthorizationState{
ClientID: "client1",
Subject: "id1",
AMR: []string{"password"},
AuthTime: time.Now(),
Scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID},
},
wantAccessToken: true,
wantRefreshToken: true,
wantIDToken: true,
},
{
name: "id token creation error",
tokenRequest: &op.DeviceAuthorizationState{
ClientID: "client1",
Subject: "foobar",
AMR: []string{"password"},
AuthTime: time.Now(),
Scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := testProvider.Storage().GetClientByClientID(context.Background(), "native")
require.NoError(t, err)
got, err := op.CreateDeviceTokenResponse(context.Background(), tt.tokenRequest, testProvider, client)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.InDelta(t, 300, got.ExpiresIn, 2)
if tt.wantAccessToken {
assert.NotEmpty(t, got.AccessToken, "access token")
}
if tt.wantRefreshToken {
assert.NotEmpty(t, got.RefreshToken, "refresh token")
}
if tt.wantIDToken {
assert.NotEmpty(t, got.IDToken, "id token")
}
})
}
}

View file

@ -4,10 +4,10 @@ import (
"context" "context"
"net/http" "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"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
) )
type DiscoverStorage interface { type DiscoverStorage interface {
@ -45,6 +45,7 @@ func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage Di
EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer), EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer),
JwksURI: config.KeysEndpoint().Absolute(issuer), JwksURI: config.KeysEndpoint().Absolute(issuer),
DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer), DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer),
CheckSessionIframe: config.CheckSessionIframe().Absolute(issuer),
ScopesSupported: Scopes(config), ScopesSupported: Scopes(config),
ResponseTypesSupported: ResponseTypes(config), ResponseTypesSupported: ResponseTypes(config),
GrantTypesSupported: GrantTypes(config), GrantTypesSupported: GrantTypes(config),
@ -61,6 +62,8 @@ func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage Di
CodeChallengeMethodsSupported: CodeChallengeMethods(config), CodeChallengeMethodsSupported: CodeChallengeMethods(config),
UILocalesSupported: config.SupportedUILocales(), UILocalesSupported: config.SupportedUILocales(),
RequestParameterSupported: config.RequestObjectSupported(), RequestParameterSupported: config.RequestObjectSupported(),
BackChannelLogoutSupported: config.BackChannelLogoutSupported(),
BackChannelLogoutSessionSupported: config.BackChannelLogoutSessionSupported(),
} }
} }
@ -92,11 +95,17 @@ func createDiscoveryConfigV2(ctx context.Context, config Configuration, storage
CodeChallengeMethodsSupported: CodeChallengeMethods(config), CodeChallengeMethodsSupported: CodeChallengeMethods(config),
UILocalesSupported: config.SupportedUILocales(), UILocalesSupported: config.SupportedUILocales(),
RequestParameterSupported: config.RequestObjectSupported(), RequestParameterSupported: config.RequestObjectSupported(),
BackChannelLogoutSupported: config.BackChannelLogoutSupported(),
BackChannelLogoutSessionSupported: config.BackChannelLogoutSessionSupported(),
} }
} }
func Scopes(c Configuration) []string { 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 { func ResponseTypes(c Configuration) []string {
@ -131,10 +140,13 @@ func GrantTypes(c Configuration) []oidc.GrantType {
} }
func SubjectTypes(c Configuration) []string { func SubjectTypes(c Configuration) []string {
return []string{"public"} //TODO: config return []string{"public"} // TODO: config
} }
func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string { func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string {
ctx, span := tracer.Start(ctx, "SigAlgorithms")
defer span.End()
algorithms, err := storage.SignatureAlgorithms(ctx) algorithms, err := storage.SignatureAlgorithms(ctx)
if err != nil { if err != nil {
return nil return nil
@ -213,32 +225,12 @@ func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod {
} }
func SupportedClaims(c Configuration) []string { func SupportedClaims(c Configuration) []string {
return []string{ // TODO: config provider, ok := c.(*Provider)
"sub", if ok && provider.config.SupportedClaims != nil {
"aud", return provider.config.SupportedClaims
"exp",
"iat",
"iss",
"auth_time",
"nonce",
"acr",
"amr",
"c_hash",
"at_hash",
"act",
"scopes",
"client_id",
"azp",
"preferred_username",
"name",
"family_name",
"given_name",
"locale",
"email",
"email_verified",
"phone_number",
"phone_number_verified",
} }
return DefaultSupportedClaims
} }
func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {

View file

@ -6,14 +6,14 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
jose "github.com/go-jose/go-jose/v3" jose "github.com/go-jose/go-jose/v4"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
"github.com/zitadel/oidc/v3/pkg/op/mock" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/op/mock"
) )
func TestDiscover(t *testing.T) { func TestDiscover(t *testing.T) {
@ -81,6 +81,11 @@ func Test_scopes(t *testing.T) {
args{}, args{},
op.DefaultSupportedScopes, op.DefaultSupportedScopes,
}, },
{
"custom scopes",
args{newTestProvider(&op.Config{SupportedScopes: []string{"test1", "test2"}})},
[]string{"test1", "test2"},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View file

@ -3,8 +3,8 @@ package op_test
import ( import (
"testing" "testing"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/op"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/op"
) )
func TestEndpoint_Path(t *testing.T) { func TestEndpoint_Path(t *testing.T) {

View file

@ -4,11 +4,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "git.christmann.info/LARA/zitadel-oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"golang.org/x/exp/slog"
) )
type ErrAuthRequest interface { type ErrAuthRequest interface {
@ -46,6 +46,12 @@ func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthReq
return return
} }
e.State = authReq.GetState() e.State = authReq.GetState()
var sessionState string
authRequestSessionState, ok := authReq.(AuthRequestSessionState)
if ok {
sessionState = authRequestSessionState.GetSessionState()
}
e.SessionState = sessionState
var responseMode oidc.ResponseMode var responseMode oidc.ResponseMode
if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok { if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok {
responseMode = rm.GetResponseMode() responseMode = rm.GetResponseMode()
@ -92,6 +98,12 @@ func TryErrorRedirect(ctx context.Context, authReq ErrAuthRequest, parent error,
} }
e.State = authReq.GetState() e.State = authReq.GetState()
var sessionState string
authRequestSessionState, ok := authReq.(AuthRequestSessionState)
if ok {
sessionState = authRequestSessionState.GetSessionState()
}
e.SessionState = sessionState
var responseMode oidc.ResponseMode var responseMode oidc.ResponseMode
if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok { if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok {
responseMode = rm.GetResponseMode() responseMode = rm.GetResponseMode()
@ -157,13 +169,29 @@ func (e StatusError) Is(err error) bool {
e.statusCode == target.statusCode e.statusCode == target.statusCode
} }
// WriteError asserts for a StatusError containing an [oidc.Error]. // WriteError asserts for a [StatusError] containing an [oidc.Error].
// If no StatusError is found, the status code will default to [http.StatusBadRequest]. // If no `StatusError` is found, the status code will default to [http.StatusBadRequest].
// If no [oidc.Error] was found in the parent, the error type defaults to [oidc.ServerError]. // If no `oidc.Error` was found in the parent, the error type defaults to [oidc.ServerError].
// When there was no `StatusError` and the `oidc.Error` is of type `oidc.ServerError`,
// the status code will be set to [http.StatusInternalServerError]
func WriteError(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger) { func WriteError(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger) {
statusError := AsStatusError(err, http.StatusBadRequest) var statusError StatusError
e := oidc.DefaultToServerError(statusError.parent, statusError.parent.Error()) if errors.As(err, &statusError) {
writeError(w, r,
logger.Log(r.Context(), e.LogLevel(), "request error", "oidc_error", e) oidc.DefaultToServerError(statusError.parent, statusError.parent.Error()),
httphelper.MarshalJSONWithStatus(w, e, statusError.statusCode) statusError.statusCode, logger,
)
return
}
statusCode := http.StatusBadRequest
e := oidc.DefaultToServerError(err, err.Error())
if e.ErrorType == oidc.ServerError {
statusCode = http.StatusInternalServerError
}
writeError(w, r, e, statusCode, logger)
}
func writeError(w http.ResponseWriter, r *http.Request, err *oidc.Error, statusCode int, logger *slog.Logger) {
logger.Log(r.Context(), err.LogLevel(), "request error", "oidc_error", err, "status_code", statusCode)
httphelper.MarshalJSONWithStatus(w, err, statusCode)
} }

View file

@ -4,17 +4,17 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
"git.christmann.info/LARA/zitadel-oidc/v3/pkg/oidc"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/schema" "github.com/zitadel/schema"
"golang.org/x/exp/slog"
) )
func TestAuthRequestError(t *testing.T) { func TestAuthRequestError(t *testing.T) {
@ -428,7 +428,8 @@ func TestTryErrorRedirect(t *testing.T) {
parent: oidc.ErrInteractionRequired().WithDescription("sign in"), parent: oidc.ErrInteractionRequired().WithDescription("sign in"),
}, },
want: &Redirect{ 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: `{ wantLog: `{
"level":"WARN", "level":"WARN",
@ -579,7 +580,7 @@ func TestWriteError(t *testing.T) {
{ {
name: "not a status or oidc error", name: "not a status or oidc error",
err: io.ErrClosedPipe, err: io.ErrClosedPipe,
wantStatus: http.StatusBadRequest, wantStatus: http.StatusInternalServerError,
wantBody: `{ wantBody: `{
"error":"server_error", "error":"server_error",
"error_description":"io: read/write on closed pipe" "error_description":"io: read/write on closed pipe"
@ -592,6 +593,7 @@ func TestWriteError(t *testing.T) {
"parent":"io: read/write on closed pipe", "parent":"io: read/write on closed pipe",
"type":"server_error" "type":"server_error"
}, },
"status_code":500,
"time":"not" "time":"not"
}`, }`,
}, },
@ -611,6 +613,7 @@ func TestWriteError(t *testing.T) {
"parent":"io: read/write on closed pipe", "parent":"io: read/write on closed pipe",
"type":"server_error" "type":"server_error"
}, },
"status_code":500,
"time":"not" "time":"not"
}`, }`,
}, },
@ -629,6 +632,7 @@ func TestWriteError(t *testing.T) {
"description":"oops", "description":"oops",
"type":"invalid_request" "type":"invalid_request"
}, },
"status_code":400,
"time":"not" "time":"not"
}`, }`,
}, },
@ -650,6 +654,7 @@ func TestWriteError(t *testing.T) {
"description":"oops", "description":"oops",
"type":"unauthorized_client" "type":"unauthorized_client"
}, },
"status_code":401,
"time":"not" "time":"not"
}`, }`,
}, },

View 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>

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