Merge branch 'main' into upgrade-guide

This commit is contained in:
Tim Möhlmann 2024-02-26 16:29:28 +02:00
commit 79a1bb9a43
47 changed files with 1464 additions and 410 deletions

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

@ -4,15 +4,40 @@ on:
issues: issues:
types: types:
- opened - opened
pull_request_target:
types:
- opened
jobs: jobs:
add-to-project: add-to-project:
name: Add issue to project name: Add issue and community pr to project
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/add-to-project@v0.5.0 - name: add issue
uses: actions/add-to-project@v0.5.0
if: ${{ github.event_name == 'issues' }}
with: with:
# You can target a repository in a different organization # You can target a repository in a different organization
# to the issue # to the issue
project-url: https://github.com/orgs/zitadel/projects/2 project-url: https://github.com/orgs/zitadel/projects/2
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 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@v0.5.0
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

@ -2,6 +2,7 @@ name: Release
on: on:
push: push:
branches: branches:
- "2.11.x"
- main - main
- next - next
tags-ignore: tags-ignore:
@ -22,11 +23,11 @@ jobs:
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@v4.0.1
with: with:
file: ./profile.cov file: ./profile.cov
name: codecov-go name: codecov-go

View file

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

View file

@ -72,7 +72,7 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid
| 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] |

View file

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

View file

@ -1,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 (
@ -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

@ -80,7 +80,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.NewLegacyServer(provider, *op.DefaultEndpoints) handler = op.RegisterLegacyServer(op.NewLegacyServer(provider, *op.DefaultEndpoints))
} }
// 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

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

29
go.mod
View file

@ -3,38 +3,39 @@ module github.com/zitadel/oidc/v3
go 1.19 go 1.19
require ( require (
github.com/go-chi/chi/v5 v5.0.10 github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/go-jose/go-jose/v3 v3.0.0 github.com/go-chi/chi/v5 v5.0.12
github.com/go-jose/go-jose/v3 v3.0.2
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.3.1 github.com/google/uuid v1.6.0
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.2
github.com/jeremija/gosubmit v0.2.7 github.com/jeremija/gosubmit v0.2.7
github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/gu v0.3.1
github.com/muhlemmer/httpforwarded v0.1.0 github.com/muhlemmer/httpforwarded v0.1.0
github.com/rs/cors v1.10.1 github.com/rs/cors v1.10.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.8.4
github.com/zitadel/logging v0.4.0 github.com/zitadel/logging v0.5.0
github.com/zitadel/schema v1.3.0 github.com/zitadel/schema v1.3.0
go.opentelemetry.io/otel v1.19.0 go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/trace v1.19.0 go.opentelemetry.io/otel/trace v1.24.0
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
golang.org/x/oauth2 v0.13.0 golang.org/x/oauth2 v0.17.0
golang.org/x/text v0.13.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.2.4 // indirect github.com/go-logr/logr v1.4.1 // 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/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.19.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect
golang.org/x/crypto v0.14.0 // indirect golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.17.0 // indirect golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.13.0 // indirect golang.org/x/sys v0.17.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.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

89
go.sum
View file

@ -1,13 +1,15 @@
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.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.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.2 h1:2Edjn8Nrb44UvTdp84KU0bBPs1cO7noRCybtS3eJEUQ=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-jose/go-jose/v3 v3.0.2/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
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.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1/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=
@ -17,19 +19,20 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 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 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
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/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@ -45,59 +48,81 @@ github.com/rs/cors v1.10.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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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.4.0 h1:lRAIFgaRoJpLNbsL7jtIYHcMDoEJP9QZB4GqMfl4xaA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zitadel/logging v0.4.0/go.mod h1:6uALRJawpkkuUPCkgzfgcPR3c2N908wqnOnIrRelUFc= github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA=
github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE=
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= 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/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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/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.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.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.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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/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=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -21,6 +21,7 @@ import (
"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/exp/slog"
"golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/example/server/exampleop" "github.com/zitadel/oidc/v3/example/server/exampleop"
"github.com/zitadel/oidc/v3/example/server/storage" "github.com/zitadel/oidc/v3/example/server/storage"
@ -217,6 +218,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 +325,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")

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

@ -4,16 +4,16 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
jose "github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v3"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"golang.org/x/exp/slog" "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" "github.com/zitadel/oidc/v3/pkg/client"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v3/pkg/http"
@ -66,19 +66,28 @@ type RelyingParty interface {
// 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
@ -91,7 +100,10 @@ type relyingParty struct {
httpClient *http.Client httpClient *http.Client
cookieHandler *httphelper.CookieHandler cookieHandler *httphelper.CookieHandler
oauthAuthStyle oauth2.AuthStyle
errorHandler func(http.ResponseWriter, *http.Request, string, string, string) errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
unauthorizedHandler func(http.ResponseWriter, *http.Request, string, string)
idTokenVerifier *IDTokenVerifier idTokenVerifier *IDTokenVerifier
verifierOpts []VerifierOption verifierOpts []VerifierOption
signer jose.Signer signer jose.Signer
@ -156,6 +168,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 {
@ -172,6 +191,8 @@ func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingPart
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 +201,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
} }
@ -201,6 +225,7 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
}, },
httpClient: httphelper.DefaultHTTPClient, httpClient: httphelper.DefaultHTTPClient,
oauth2Only: false, oauth2Only: false,
oauthAuthStyle: oauth2.AuthStyleAutoDetect,
} }
for _, optFunc := range options { for _, optFunc := range options {
@ -217,9 +242,13 @@ func NewRelyingPartyOIDC(ctx context.Context, issuer, clientID, clientSecret, re
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 +297,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
@ -355,13 +398,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))
@ -416,17 +459,39 @@ func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingP
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 = 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) {
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() params := r.URL.Query()
@ -442,7 +507,7 @@ 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))
@ -451,14 +516,14 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R
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()}, 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(), params.Get("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)
@ -478,7 +543,7 @@ func UserinfoCallback[C oidc.IDClaims, U SubjectGetter](f CodeExchangeUserinfoCa
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) {
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)
@ -546,7 +611,6 @@ 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,
@ -703,5 +767,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

@ -4,14 +4,19 @@ import (
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors"
) )
func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { func BytesToPrivateKey(b []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(priv) block, _ := pem.Decode(b)
b := block.Bytes if block == nil {
key, err := x509.ParsePKCS1PrivateKey(b) return nil, errors.New("PEM decode failed")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return key, nil return key, nil
} }

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

@ -0,0 +1,62 @@
package crypto_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/oidc/v3/pkg/crypto"
)
func TestBytesToPrivateKey(tt *testing.T) {
tt.Run("PEMDecodeError", func(t *testing.T) {
_, err := crypto.BytesToPrivateKey([]byte("The non-PEM sequence"))
assert.EqualError(t, err, "PEM decode failed")
})
tt.Run("InvalidKeyFormat", func(t *testing.T) {
_, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN PRIVATE KEY-----
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-----`))
assert.EqualError(t, err, "x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)")
})
tt.Run("Ok", func(t *testing.T) {
key, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
-----END RSA PRIVATE KEY-----`))
assert.NoError(t, err)
assert.NotNil(t, key)
})
}

View file

@ -3,6 +3,7 @@ package oidc
import ( import (
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
@ -76,8 +77,23 @@ 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) 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

View file

@ -208,20 +208,46 @@ 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 {
name string
input string
want dst
wantErr bool
}{
{
name: "afrikaans, ok",
input: `{"locale": "af"}`,
want: dst{
Locale: NewLocale(language.Afrikaans), Locale: NewLocale(language.Afrikaans),
},
},
{
name: "gb, ignored",
input: `{"locale": "gb"}`,
want: dst{
Locale: &Locale{},
},
},
{
name: "bad form, error",
input: `{"locale": "g!!!!!"}`,
wantErr: true,
},
} }
const input = `{"locale": "af"}` for _, tt := range tests {
var got a var got dst
err := json.Unmarshal([]byte(tt.input), &got)
require.NoError(t, if tt.wantErr {
json.Unmarshal([]byte(input), &got), require.Error(t, err)
) return
assert.Equal(t, want, got) }
require.NoError(t, err)
assert.Equal(t, tt.want, got)
}
} }
func TestParseLocales(t *testing.T) { func TestParseLocales(t *testing.T) {

View file

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

View file

@ -7,10 +7,10 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"path"
"strings" "strings"
"time" "time"
"github.com/bmatcuk/doublestar/v4"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
str "github.com/zitadel/oidc/v3/pkg/strings" str "github.com/zitadel/oidc/v3/pkg/strings"
@ -138,20 +138,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 !str.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
@ -283,7 +283,7 @@ func checkURIAgainstRedirects(client Client, uri string) error {
} }
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)
} }
@ -391,9 +391,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
} }

View file

@ -583,6 +583,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) {

View file

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

View file

@ -54,7 +54,24 @@ type Configuration interface {
type IssuerFromRequest func(r *http.Request) string type IssuerFromRequest func(r *http.Request) string
func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) { func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) {
return issuerFromForwardedOrHost(path, false) return issuerFromForwardedOrHost(path, new(issuerConfig))
}
type IssuerFromOption func(c *issuerConfig)
// WithIssuerFromCustomHeaders can be used to customize the header names used.
// The same rules apply where the first successful host is returned.
func WithIssuerFromCustomHeaders(headers ...string) IssuerFromOption {
return func(c *issuerConfig) {
for i, h := range headers {
headers[i] = http.CanonicalHeaderKey(h)
}
c.headers = headers
}
}
type issuerConfig struct {
headers []string
} }
// IssuerFromForwardedOrHost tries to establish the Issuer based // IssuerFromForwardedOrHost tries to establish the Issuer based
@ -64,11 +81,18 @@ func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) {
// If the Forwarded header is not present, no host field is found, // If the Forwarded header is not present, no host field is found,
// or there is a parser error the Request Host will be used as a fallback. // or there is a parser error the Request Host will be used as a fallback.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
func IssuerFromForwardedOrHost(path string) func(bool) (IssuerFromRequest, error) { func IssuerFromForwardedOrHost(path string, opts ...IssuerFromOption) func(bool) (IssuerFromRequest, error) {
return issuerFromForwardedOrHost(path, true) c := &issuerConfig{
headers: []string{http.CanonicalHeaderKey("forwarded")},
}
for _, opt := range opts {
opt(c)
} }
func issuerFromForwardedOrHost(path string, parseForwarded bool) func(bool) (IssuerFromRequest, error) { return issuerFromForwardedOrHost(path, c)
}
func issuerFromForwardedOrHost(path string, c *issuerConfig) func(bool) (IssuerFromRequest, error) {
return func(allowInsecure bool) (IssuerFromRequest, error) { return func(allowInsecure bool) (IssuerFromRequest, error) {
issuerPath, err := url.Parse(path) issuerPath, err := url.Parse(path)
if err != nil { if err != nil {
@ -78,26 +102,26 @@ func issuerFromForwardedOrHost(path string, parseForwarded bool) func(bool) (Iss
return nil, err return nil, err
} }
return func(r *http.Request) string { return func(r *http.Request) string {
if parseForwarded { if host, ok := hostFromForwarded(r, c.headers); ok {
if host, ok := hostFromForwarded(r); ok {
return dynamicIssuer(host, path, allowInsecure) return dynamicIssuer(host, path, allowInsecure)
} }
}
return dynamicIssuer(r.Host, path, allowInsecure) return dynamicIssuer(r.Host, path, allowInsecure)
}, nil }, nil
} }
} }
func hostFromForwarded(r *http.Request) (host string, ok bool) { func hostFromForwarded(r *http.Request, headers []string) (host string, ok bool) {
fwd, err := httpforwarded.ParseFromRequest(r) for _, header := range headers {
hosts, err := httpforwarded.ParseParameter("host", r.Header[header])
if err != nil { if err != nil {
log.Printf("Err: issuer from forwarded header: %v", err) // TODO change to slog on next branch log.Printf("Err: issuer from forwarded header: %v", err) // TODO change to slog on next branch
return "", false continue
} }
if fwd == nil || len(fwd["host"]) == 0 { if len(hosts) > 0 {
return "", false return hosts[0], true
} }
return fwd["host"][0], true }
return "", false
} }
func StaticIssuer(issuer string) func(bool) (IssuerFromRequest, error) { func StaticIssuer(issuer string) func(bool) (IssuerFromRequest, error) {

View file

@ -1,6 +1,7 @@
package op package op
import ( import (
"net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"testing" "testing"
@ -265,8 +266,9 @@ func TestIssuerFromHost(t *testing.T) {
func TestIssuerFromForwardedOrHost(t *testing.T) { func TestIssuerFromForwardedOrHost(t *testing.T) {
type args struct { type args struct {
path string path string
opts []IssuerFromOption
target string target string
forwarded []string header map[string][]string
} }
type res struct { type res struct {
issuer string issuer string
@ -281,7 +283,7 @@ func TestIssuerFromForwardedOrHost(t *testing.T) {
args{ args{
path: "/custom/", path: "/custom/",
target: "https://issuer.com", target: "https://issuer.com",
forwarded: []string{"~~~"}, header: map[string][]string{"Forwarded": {"~~~~"}},
}, },
res{ res{
issuer: "https://issuer.com/custom/", issuer: "https://issuer.com/custom/",
@ -303,9 +305,9 @@ func TestIssuerFromForwardedOrHost(t *testing.T) {
args{ args{
path: "/custom/", path: "/custom/",
target: "https://issuer.com", target: "https://issuer.com",
forwarded: []string{ header: map[string][]string{"Forwarded": {
`by=identifier;for=identifier;proto=https`, `by=identifier;for=identifier;proto=https`,
}, }},
}, },
res{ res{
issuer: "https://issuer.com/custom/", issuer: "https://issuer.com/custom/",
@ -316,9 +318,9 @@ func TestIssuerFromForwardedOrHost(t *testing.T) {
args{ args{
path: "/custom/", path: "/custom/",
target: "https://issuer.com", target: "https://issuer.com",
forwarded: []string{ header: map[string][]string{"Forwarded": {
`by=identifier;for=identifier;host=first.com;proto=https`, `by=identifier;for=identifier;host=first.com;proto=https`,
}, }},
}, },
res{ res{
issuer: "https://first.com/custom/", issuer: "https://first.com/custom/",
@ -329,9 +331,9 @@ func TestIssuerFromForwardedOrHost(t *testing.T) {
args{ args{
path: "/custom/", path: "/custom/",
target: "https://issuer.com", target: "https://issuer.com",
forwarded: []string{ header: map[string][]string{"Forwarded": {
`by=identifier;for=identifier;host=first.com;proto=https,host=second.com`, `by=identifier;for=identifier;host=first.com;proto=https,host=second.com`,
}, }},
}, },
res{ res{
issuer: "https://first.com/custom/", issuer: "https://first.com/custom/",
@ -342,23 +344,45 @@ func TestIssuerFromForwardedOrHost(t *testing.T) {
args{ args{
path: "/custom/", path: "/custom/",
target: "https://issuer.com", target: "https://issuer.com",
forwarded: []string{ header: map[string][]string{"Forwarded": {
`by=identifier;for=identifier;host=first.com;proto=https,host=second.com`, `by=identifier;for=identifier;host=first.com;proto=https,host=second.com`,
`by=identifier;for=identifier;host=third.com;proto=https`, `by=identifier;for=identifier;host=third.com;proto=https`,
}, }},
}, },
res{ res{
issuer: "https://first.com/custom/", issuer: "https://first.com/custom/",
}, },
}, },
{
"custom header first",
args{
path: "/custom/",
target: "https://issuer.com",
header: map[string][]string{
"Forwarded": {
`by=identifier;for=identifier;host=first.com;proto=https,host=second.com`,
`by=identifier;for=identifier;host=third.com;proto=https`,
},
"X-Custom-Forwarded": {
`by=identifier;for=identifier;host=custom.com;proto=https,host=custom2.com`,
},
},
opts: []IssuerFromOption{
WithIssuerFromCustomHeaders("x-custom-forwarded"),
},
},
res{
issuer: "https://custom.com/custom/",
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
issuer, err := IssuerFromForwardedOrHost(tt.args.path)(false) issuer, err := IssuerFromForwardedOrHost(tt.args.path, tt.args.opts...)(false)
require.NoError(t, err) require.NoError(t, err)
req := httptest.NewRequest("", tt.args.target, nil) req := httptest.NewRequest("", tt.args.target, nil)
if tt.args.forwarded != nil { for k, v := range tt.args.header {
req.Header["Forwarded"] = tt.args.forwarded req.Header[http.CanonicalHeaderKey(k)] = v
} }
assert.Equal(t, tt.res.issuer, issuer(req)) assert.Equal(t, tt.res.issuer, issuer(req))
}) })

View file

@ -14,6 +14,7 @@ import (
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
strs "github.com/zitadel/oidc/v3/pkg/strings"
) )
type DeviceAuthorizationConfig struct { type DeviceAuthorizationConfig struct {
@ -185,24 +186,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 +212,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 +226,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,6 +243,50 @@ 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 !strs.Contains(r.Audience, r.ClientID) {
r.Audience = append(r.Audience, r.ClientID)
}
return r.Audience
}
func (r *DeviceAuthorizationState) GetAuthTime() time.Time {
return r.AuthTime
}
func (r *DeviceAuthorizationState) GetClientID() string {
return r.ClientID
}
func (r *DeviceAuthorizationState) GetScopes() []string {
return r.Scopes
}
func (r *DeviceAuthorizationState) GetSubject() string {
return r.Subject
}
func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) { func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) {
storage, err := assertDeviceStorage(exchanger.Storage()) storage, err := assertDeviceStorage(exchanger.Storage())
if err != nil { if err != nil {
@ -291,15 +313,32 @@ 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 }
// TODO(v4): remove type assertion
if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && strs.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) {
response.IDToken, err = CreateIDToken(ctx, IssuerFromContext(ctx), idTokenRequest, client.IDTokenLifetime(), accessToken, "", creator.Storage(), client)
if err != nil {
return nil, err
}
}
return response, nil
} }

View file

@ -453,3 +453,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

@ -213,32 +213,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

@ -157,13 +157,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

@ -579,7 +579,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 +592,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 +612,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 +631,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 +653,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

@ -4,6 +4,7 @@ package mock
//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v3/pkg/op Storage //go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v3/pkg/op Storage
//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v3/pkg/op Authorizer //go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v3/pkg/op Authorizer
//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v3/pkg/op Client //go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v3/pkg/op Client
//go:generate mockgen -package mock -destination ./glob.mock.go github.com/zitadel/oidc/v3/pkg/op HasRedirectGlobs
//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v3/pkg/op Configuration //go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v3/pkg/op Configuration
//go:generate mockgen -package mock -destination ./discovery.mock.go github.com/zitadel/oidc/v3/pkg/op DiscoverStorage //go:generate mockgen -package mock -destination ./discovery.mock.go github.com/zitadel/oidc/v3/pkg/op DiscoverStorage
//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/v3/pkg/op SigningKey,Key //go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/v3/pkg/op SigningKey,Key

24
pkg/op/mock/glob.go Normal file
View file

@ -0,0 +1,24 @@
package mock
import (
"testing"
gomock "github.com/golang/mock/gomock"
"github.com/zitadel/oidc/v3/pkg/oidc"
op "github.com/zitadel/oidc/v3/pkg/op"
)
func NewHasRedirectGlobs(t *testing.T) op.HasRedirectGlobs {
return NewMockHasRedirectGlobs(gomock.NewController(t))
}
func NewHasRedirectGlobsWithConfig(t *testing.T, uri []string, appType op.ApplicationType, responseTypes []oidc.ResponseType, devMode bool) op.HasRedirectGlobs {
c := NewHasRedirectGlobs(t)
m := c.(*MockHasRedirectGlobs)
m.EXPECT().RedirectURIs().AnyTimes().Return(uri)
m.EXPECT().RedirectURIGlobs().AnyTimes().Return(uri)
m.EXPECT().ApplicationType().AnyTimes().Return(appType)
m.EXPECT().ResponseTypes().AnyTimes().Return(responseTypes)
m.EXPECT().DevMode().AnyTimes().Return(devMode)
return c
}

289
pkg/op/mock/glob.mock.go Normal file
View file

@ -0,0 +1,289 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/zitadel/oidc/v3/pkg/op (interfaces: HasRedirectGlobs)
// Package mock is a generated GoMock package.
package mock
import (
reflect "reflect"
time "time"
gomock "github.com/golang/mock/gomock"
oidc "github.com/zitadel/oidc/v3/pkg/oidc"
op "github.com/zitadel/oidc/v3/pkg/op"
)
// MockHasRedirectGlobs is a mock of HasRedirectGlobs interface.
type MockHasRedirectGlobs struct {
ctrl *gomock.Controller
recorder *MockHasRedirectGlobsMockRecorder
}
// MockHasRedirectGlobsMockRecorder is the mock recorder for MockHasRedirectGlobs.
type MockHasRedirectGlobsMockRecorder struct {
mock *MockHasRedirectGlobs
}
// NewMockHasRedirectGlobs creates a new mock instance.
func NewMockHasRedirectGlobs(ctrl *gomock.Controller) *MockHasRedirectGlobs {
mock := &MockHasRedirectGlobs{ctrl: ctrl}
mock.recorder = &MockHasRedirectGlobsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockHasRedirectGlobs) EXPECT() *MockHasRedirectGlobsMockRecorder {
return m.recorder
}
// AccessTokenType mocks base method.
func (m *MockHasRedirectGlobs) AccessTokenType() op.AccessTokenType {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AccessTokenType")
ret0, _ := ret[0].(op.AccessTokenType)
return ret0
}
// AccessTokenType indicates an expected call of AccessTokenType.
func (mr *MockHasRedirectGlobsMockRecorder) AccessTokenType() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccessTokenType", reflect.TypeOf((*MockHasRedirectGlobs)(nil).AccessTokenType))
}
// ApplicationType mocks base method.
func (m *MockHasRedirectGlobs) ApplicationType() op.ApplicationType {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplicationType")
ret0, _ := ret[0].(op.ApplicationType)
return ret0
}
// ApplicationType indicates an expected call of ApplicationType.
func (mr *MockHasRedirectGlobsMockRecorder) ApplicationType() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplicationType", reflect.TypeOf((*MockHasRedirectGlobs)(nil).ApplicationType))
}
// AuthMethod mocks base method.
func (m *MockHasRedirectGlobs) AuthMethod() oidc.AuthMethod {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthMethod")
ret0, _ := ret[0].(oidc.AuthMethod)
return ret0
}
// AuthMethod indicates an expected call of AuthMethod.
func (mr *MockHasRedirectGlobsMockRecorder) AuthMethod() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethod", reflect.TypeOf((*MockHasRedirectGlobs)(nil).AuthMethod))
}
// ClockSkew mocks base method.
func (m *MockHasRedirectGlobs) ClockSkew() time.Duration {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClockSkew")
ret0, _ := ret[0].(time.Duration)
return ret0
}
// ClockSkew indicates an expected call of ClockSkew.
func (mr *MockHasRedirectGlobsMockRecorder) ClockSkew() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClockSkew", reflect.TypeOf((*MockHasRedirectGlobs)(nil).ClockSkew))
}
// DevMode mocks base method.
func (m *MockHasRedirectGlobs) DevMode() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DevMode")
ret0, _ := ret[0].(bool)
return ret0
}
// DevMode indicates an expected call of DevMode.
func (mr *MockHasRedirectGlobsMockRecorder) DevMode() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DevMode", reflect.TypeOf((*MockHasRedirectGlobs)(nil).DevMode))
}
// GetID mocks base method.
func (m *MockHasRedirectGlobs) GetID() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetID")
ret0, _ := ret[0].(string)
return ret0
}
// GetID indicates an expected call of GetID.
func (mr *MockHasRedirectGlobsMockRecorder) GetID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockHasRedirectGlobs)(nil).GetID))
}
// GrantTypes mocks base method.
func (m *MockHasRedirectGlobs) GrantTypes() []oidc.GrantType {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GrantTypes")
ret0, _ := ret[0].([]oidc.GrantType)
return ret0
}
// GrantTypes indicates an expected call of GrantTypes.
func (mr *MockHasRedirectGlobsMockRecorder) GrantTypes() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypes", reflect.TypeOf((*MockHasRedirectGlobs)(nil).GrantTypes))
}
// IDTokenLifetime mocks base method.
func (m *MockHasRedirectGlobs) IDTokenLifetime() time.Duration {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IDTokenLifetime")
ret0, _ := ret[0].(time.Duration)
return ret0
}
// IDTokenLifetime indicates an expected call of IDTokenLifetime.
func (mr *MockHasRedirectGlobsMockRecorder) IDTokenLifetime() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenLifetime", reflect.TypeOf((*MockHasRedirectGlobs)(nil).IDTokenLifetime))
}
// IDTokenUserinfoClaimsAssertion mocks base method.
func (m *MockHasRedirectGlobs) IDTokenUserinfoClaimsAssertion() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IDTokenUserinfoClaimsAssertion")
ret0, _ := ret[0].(bool)
return ret0
}
// IDTokenUserinfoClaimsAssertion indicates an expected call of IDTokenUserinfoClaimsAssertion.
func (mr *MockHasRedirectGlobsMockRecorder) IDTokenUserinfoClaimsAssertion() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenUserinfoClaimsAssertion", reflect.TypeOf((*MockHasRedirectGlobs)(nil).IDTokenUserinfoClaimsAssertion))
}
// IsScopeAllowed mocks base method.
func (m *MockHasRedirectGlobs) IsScopeAllowed(arg0 string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsScopeAllowed", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// IsScopeAllowed indicates an expected call of IsScopeAllowed.
func (mr *MockHasRedirectGlobsMockRecorder) IsScopeAllowed(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsScopeAllowed", reflect.TypeOf((*MockHasRedirectGlobs)(nil).IsScopeAllowed), arg0)
}
// LoginURL mocks base method.
func (m *MockHasRedirectGlobs) LoginURL(arg0 string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoginURL", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// LoginURL indicates an expected call of LoginURL.
func (mr *MockHasRedirectGlobsMockRecorder) LoginURL(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoginURL", reflect.TypeOf((*MockHasRedirectGlobs)(nil).LoginURL), arg0)
}
// PostLogoutRedirectURIGlobs mocks base method.
func (m *MockHasRedirectGlobs) PostLogoutRedirectURIGlobs() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PostLogoutRedirectURIGlobs")
ret0, _ := ret[0].([]string)
return ret0
}
// PostLogoutRedirectURIGlobs indicates an expected call of PostLogoutRedirectURIGlobs.
func (mr *MockHasRedirectGlobsMockRecorder) PostLogoutRedirectURIGlobs() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostLogoutRedirectURIGlobs", reflect.TypeOf((*MockHasRedirectGlobs)(nil).PostLogoutRedirectURIGlobs))
}
// PostLogoutRedirectURIs mocks base method.
func (m *MockHasRedirectGlobs) PostLogoutRedirectURIs() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PostLogoutRedirectURIs")
ret0, _ := ret[0].([]string)
return ret0
}
// PostLogoutRedirectURIs indicates an expected call of PostLogoutRedirectURIs.
func (mr *MockHasRedirectGlobsMockRecorder) PostLogoutRedirectURIs() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostLogoutRedirectURIs", reflect.TypeOf((*MockHasRedirectGlobs)(nil).PostLogoutRedirectURIs))
}
// RedirectURIGlobs mocks base method.
func (m *MockHasRedirectGlobs) RedirectURIGlobs() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RedirectURIGlobs")
ret0, _ := ret[0].([]string)
return ret0
}
// RedirectURIGlobs indicates an expected call of RedirectURIGlobs.
func (mr *MockHasRedirectGlobsMockRecorder) RedirectURIGlobs() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RedirectURIGlobs", reflect.TypeOf((*MockHasRedirectGlobs)(nil).RedirectURIGlobs))
}
// RedirectURIs mocks base method.
func (m *MockHasRedirectGlobs) RedirectURIs() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RedirectURIs")
ret0, _ := ret[0].([]string)
return ret0
}
// RedirectURIs indicates an expected call of RedirectURIs.
func (mr *MockHasRedirectGlobsMockRecorder) RedirectURIs() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RedirectURIs", reflect.TypeOf((*MockHasRedirectGlobs)(nil).RedirectURIs))
}
// ResponseTypes mocks base method.
func (m *MockHasRedirectGlobs) ResponseTypes() []oidc.ResponseType {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResponseTypes")
ret0, _ := ret[0].([]oidc.ResponseType)
return ret0
}
// ResponseTypes indicates an expected call of ResponseTypes.
func (mr *MockHasRedirectGlobsMockRecorder) ResponseTypes() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResponseTypes", reflect.TypeOf((*MockHasRedirectGlobs)(nil).ResponseTypes))
}
// RestrictAdditionalAccessTokenScopes mocks base method.
func (m *MockHasRedirectGlobs) RestrictAdditionalAccessTokenScopes() func([]string) []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RestrictAdditionalAccessTokenScopes")
ret0, _ := ret[0].(func([]string) []string)
return ret0
}
// RestrictAdditionalAccessTokenScopes indicates an expected call of RestrictAdditionalAccessTokenScopes.
func (mr *MockHasRedirectGlobsMockRecorder) RestrictAdditionalAccessTokenScopes() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalAccessTokenScopes", reflect.TypeOf((*MockHasRedirectGlobs)(nil).RestrictAdditionalAccessTokenScopes))
}
// RestrictAdditionalIdTokenScopes mocks base method.
func (m *MockHasRedirectGlobs) RestrictAdditionalIdTokenScopes() func([]string) []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RestrictAdditionalIdTokenScopes")
ret0, _ := ret[0].(func([]string) []string)
return ret0
}
// RestrictAdditionalIdTokenScopes indicates an expected call of RestrictAdditionalIdTokenScopes.
func (mr *MockHasRedirectGlobsMockRecorder) RestrictAdditionalIdTokenScopes() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalIdTokenScopes", reflect.TypeOf((*MockHasRedirectGlobs)(nil).RestrictAdditionalIdTokenScopes))
}

View file

@ -45,6 +45,33 @@ var (
DeviceAuthorization: NewEndpoint(defaultDeviceAuthzEndpoint), DeviceAuthorization: NewEndpoint(defaultDeviceAuthzEndpoint),
} }
DefaultSupportedClaims = []string{
"sub",
"aud",
"exp",
"iat",
"iss",
"auth_time",
"nonce",
"acr",
"amr",
"c_hash",
"at_hash",
"act",
"scopes",
"client_id",
"azp",
"preferred_username",
"name",
"family_name",
"given_name",
"locale",
"email",
"email_verified",
"phone_number",
"phone_number_verified",
}
defaultCORSOptions = cors.Options{ defaultCORSOptions = cors.Options{
AllowCredentials: true, AllowCredentials: true,
AllowedHeaders: []string{ AllowedHeaders: []string{
@ -97,9 +124,19 @@ type OpenIDProvider interface {
type HttpInterceptor func(http.Handler) http.Handler type HttpInterceptor func(http.Handler) http.Handler
type corsOptioner interface {
CORSOptions() *cors.Options
}
func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) chi.Router { func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) chi.Router {
router := chi.NewRouter() router := chi.NewRouter()
if co, ok := o.(corsOptioner); ok {
if opts := co.CORSOptions(); opts != nil {
router.Use(cors.New(*opts).Handler)
}
} else {
router.Use(cors.New(defaultCORSOptions).Handler) router.Use(cors.New(defaultCORSOptions).Handler)
}
router.Use(intercept(o.IssuerFromRequest, interceptors...)) router.Use(intercept(o.IssuerFromRequest, interceptors...))
router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(healthEndpoint, healthHandler)
router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes()))
@ -136,6 +173,7 @@ type Config struct {
GrantTypeRefreshToken bool GrantTypeRefreshToken bool
RequestObjectSupported bool RequestObjectSupported bool
SupportedUILocales []language.Tag SupportedUILocales []language.Tag
SupportedClaims []string
DeviceAuthorization DeviceAuthorizationConfig DeviceAuthorization DeviceAuthorizationConfig
} }
@ -173,27 +211,61 @@ type Endpoints struct {
// Successful logins should mark the request as authorized and redirect back to to // Successful logins should mark the request as authorized and redirect back to to
// op.AuthCallbackURL(provider) which is probably /callback. On the redirect back // op.AuthCallbackURL(provider) which is probably /callback. On the redirect back
// to the AuthCallbackURL, the request id should be passed as the "id" parameter. // to the AuthCallbackURL, the request id should be passed as the "id" parameter.
//
// Deprecated: use [NewProvider] with an issuer function direct.
func NewOpenIDProvider(issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { func NewOpenIDProvider(issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
return newProvider(config, storage, StaticIssuer(issuer), opOpts...) return NewProvider(config, storage, StaticIssuer(issuer), opOpts...)
} }
// NewForwardedOpenIDProvider tries to establishes the issuer from the request Host. // NewForwardedOpenIDProvider tries to establishes the issuer from the request Host.
//
// Deprecated: use [NewProvider] with an issuer function direct.
func NewDynamicOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { func NewDynamicOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
return newProvider(config, storage, IssuerFromHost(path), opOpts...) return NewProvider(config, storage, IssuerFromHost(path), opOpts...)
} }
// NewForwardedOpenIDProvider tries to establish the Issuer from a Forwarded request header, if it is set. // NewForwardedOpenIDProvider tries to establish the Issuer from a Forwarded request header, if it is set.
// See [IssuerFromForwardedOrHost] for details. // See [IssuerFromForwardedOrHost] for details.
//
// Deprecated: use [NewProvider] with an issuer function direct.
func NewForwardedOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) { func NewForwardedOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
return newProvider(config, storage, IssuerFromForwardedOrHost(path), opOpts...) return NewProvider(config, storage, IssuerFromForwardedOrHost(path), opOpts...)
} }
func newProvider(config *Config, storage Storage, issuer func(bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) { // NewProvider creates a provider with a router on it's embedded http.Handler.
// Issuer is a function that must return the issuer on every request.
// Typically [StaticIssuer], [IssuerFromHost] or [IssuerFromForwardedOrHost] can be used.
//
// The router handles a suite of endpoints (some paths can be overridden):
//
// /healthz
// /ready
// /.well-known/openid-configuration
// /oauth/token
// /oauth/introspect
// /callback
// /authorize
// /userinfo
// /revoke
// /end_session
// /keys
// /device_authorization
//
// This does not include login. Login is handled with a redirect that includes the
// request ID. The redirect for logins is specified per-client by Client.LoginURL().
// Successful logins should mark the request as authorized and redirect back to to
// op.AuthCallbackURL(provider) which is probably /callback. On the redirect back
// to the AuthCallbackURL, the request id should be passed as the "id" parameter.
func NewProvider(config *Config, storage Storage, issuer func(insecure bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) {
keySet := &OpenIDKeySet{storage}
o := &Provider{ o := &Provider{
config: config, config: config,
storage: storage, storage: storage,
accessTokenKeySet: keySet,
idTokenHinKeySet: keySet,
endpoints: DefaultEndpoints, endpoints: DefaultEndpoints,
timer: make(<-chan time.Time), timer: make(<-chan time.Time),
corsOpts: &defaultCORSOptions,
logger: slog.Default(), logger: slog.Default(),
} }
@ -207,19 +279,11 @@ func newProvider(config *Config, storage Storage, issuer func(bool) (IssuerFromR
if err != nil { if err != nil {
return nil, err return nil, err
} }
o.Handler = CreateRouter(o, o.interceptors...) o.Handler = CreateRouter(o, o.interceptors...)
o.decoder = schema.NewDecoder() o.decoder = schema.NewDecoder()
o.decoder.IgnoreUnknownKeys(true) o.decoder.IgnoreUnknownKeys(true)
o.encoder = oidc.NewEncoder() o.encoder = oidc.NewEncoder()
o.crypto = NewAESCrypto(config.CryptoKey) o.crypto = NewAESCrypto(config.CryptoKey)
// Avoid potential race conditions by calling these early
_ = o.openIDKeySet() // sets keySet
return o, nil return o, nil
} }
@ -230,7 +294,8 @@ type Provider struct {
insecure bool insecure bool
endpoints *Endpoints endpoints *Endpoints
storage Storage storage Storage
keySet *openIDKeySet accessTokenKeySet oidc.KeySet
idTokenHinKeySet oidc.KeySet
crypto Crypto crypto Crypto
decoder *schema.Decoder decoder *schema.Decoder
encoder *schema.Encoder encoder *schema.Encoder
@ -238,6 +303,7 @@ type Provider struct {
timer <-chan time.Time timer <-chan time.Time
accessTokenVerifierOpts []AccessTokenVerifierOpt accessTokenVerifierOpts []AccessTokenVerifierOpt
idTokenHintVerifierOpts []IDTokenHintVerifierOpt idTokenHintVerifierOpts []IDTokenHintVerifierOpt
corsOpts *cors.Options
logger *slog.Logger logger *slog.Logger
} }
@ -365,7 +431,7 @@ func (o *Provider) Encoder() httphelper.Encoder {
} }
func (o *Provider) IDTokenHintVerifier(ctx context.Context) *IDTokenHintVerifier { func (o *Provider) IDTokenHintVerifier(ctx context.Context) *IDTokenHintVerifier {
return NewIDTokenHintVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.idTokenHintVerifierOpts...) return NewIDTokenHintVerifier(IssuerFromContext(ctx), o.idTokenHinKeySet, o.idTokenHintVerifierOpts...)
} }
func (o *Provider) JWTProfileVerifier(ctx context.Context) *JWTProfileVerifier { func (o *Provider) JWTProfileVerifier(ctx context.Context) *JWTProfileVerifier {
@ -373,14 +439,7 @@ func (o *Provider) JWTProfileVerifier(ctx context.Context) *JWTProfileVerifier {
} }
func (o *Provider) AccessTokenVerifier(ctx context.Context) *AccessTokenVerifier { func (o *Provider) AccessTokenVerifier(ctx context.Context) *AccessTokenVerifier {
return NewAccessTokenVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.accessTokenVerifierOpts...) return NewAccessTokenVerifier(IssuerFromContext(ctx), o.accessTokenKeySet, o.accessTokenVerifierOpts...)
}
func (o *Provider) openIDKeySet() oidc.KeySet {
if o.keySet == nil {
o.keySet = &openIDKeySet{o.Storage()}
}
return o.keySet
} }
func (o *Provider) Crypto() Crypto { func (o *Provider) Crypto() Crypto {
@ -397,6 +456,10 @@ func (o *Provider) Probes() []ProbesFn {
} }
} }
func (o *Provider) CORSOptions() *cors.Options {
return o.corsOpts
}
func (o *Provider) Logger() *slog.Logger { func (o *Provider) Logger() *slog.Logger {
return o.logger return o.logger
} }
@ -406,13 +469,13 @@ func (o *Provider) HttpHandler() http.Handler {
return o return o
} }
type openIDKeySet struct { type OpenIDKeySet struct {
Storage Storage
} }
// VerifySignature implements the oidc.KeySet interface // VerifySignature implements the oidc.KeySet interface
// providing an implementation for the keys stored in the OP Storage interface // providing an implementation for the keys stored in the OP Storage interface
func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { func (o *OpenIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
keySet, err := o.Storage.KeySet(ctx) keySet, err := o.Storage.KeySet(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching keys: %w", err) return nil, fmt.Errorf("error fetching keys: %w", err)
@ -543,6 +606,15 @@ func WithHttpInterceptors(interceptors ...HttpInterceptor) Option {
} }
} }
// WithAccessTokenKeySet allows passing a KeySet with public keys for Access Token verification.
// The default KeySet uses the [Storage] interface
func WithAccessTokenKeySet(keySet oidc.KeySet) Option {
return func(o *Provider) error {
o.accessTokenKeySet = keySet
return nil
}
}
func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option { func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option {
return func(o *Provider) error { return func(o *Provider) error {
o.accessTokenVerifierOpts = opts o.accessTokenVerifierOpts = opts
@ -550,6 +622,15 @@ func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option {
} }
} }
// WithIDTokenHintKeySet allows passing a KeySet with public keys for ID Token Hint verification.
// The default KeySet uses the [Storage] interface.
func WithIDTokenHintKeySet(keySet oidc.KeySet) Option {
return func(o *Provider) error {
o.idTokenHinKeySet = keySet
return nil
}
}
func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option { func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option {
return func(o *Provider) error { return func(o *Provider) error {
o.idTokenHintVerifierOpts = opts o.idTokenHintVerifierOpts = opts
@ -557,6 +638,13 @@ func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option {
} }
} }
func WithCORSOptions(opts *cors.Options) Option {
return func(o *Provider) error {
o.corsOpts = opts
return nil
}
}
// WithLogger lets a logger other than slog.Default(). // WithLogger lets a logger other than slog.Default().
// //
// EXPERIMENTAL: Will change to log/slog import after we drop support for Go 1.20 // EXPERIMENTAL: Will change to log/slog import after we drop support for Go 1.20
@ -573,6 +661,6 @@ func intercept(i IssuerFromRequest, interceptors ...HttpInterceptor) func(handle
for i := len(interceptors) - 1; i >= 0; i-- { for i := len(interceptors) - 1; i >= 0; i-- {
handler = interceptors[i](handler) handler = interceptors[i](handler)
} }
return cors.New(defaultCORSOptions).Handler(issuerInterceptor.Handler(handler)) return issuerInterceptor.Handler(handler)
} }
} }

View file

@ -30,6 +30,7 @@ var (
AuthMethodPrivateKeyJWT: true, AuthMethodPrivateKeyJWT: true,
GrantTypeRefreshToken: true, GrantTypeRefreshToken: true,
RequestObjectSupported: true, RequestObjectSupported: true,
SupportedClaims: op.DefaultSupportedClaims,
SupportedUILocales: []language.Tag{language.English}, SupportedUILocales: []language.Tag{language.English},
DeviceAuthorization: op.DeviceAuthorizationConfig{ DeviceAuthorization: op.DeviceAuthorizationConfig{
Lifetime: 5 * time.Minute, Lifetime: 5 * time.Minute,
@ -57,8 +58,12 @@ func init() {
} }
func newTestProvider(config *op.Config) op.OpenIDProvider { func newTestProvider(config *op.Config) op.OpenIDProvider {
provider, err := op.NewOpenIDProvider(testIssuer, config, storage := storage.NewStorage(storage.NewUserStore(testIssuer))
storage.NewStorage(storage.NewUserStore(testIssuer)), op.WithAllowInsecure(), keySet := &op.OpenIDKeySet{storage}
provider, err := op.NewOpenIDProvider(testIssuer, config, storage,
op.WithAllowInsecure(),
op.WithAccessTokenKeySet(keySet),
op.WithIDTokenHintKeySet(keySet),
) )
if err != nil { if err != nil {
panic(err) panic(err)

View file

@ -127,7 +127,7 @@ type Server interface {
// Introspect handles the OAuth 2.0 Token Introspection endpoint. // Introspect handles the OAuth 2.0 Token Introspection endpoint.
// https://datatracker.ietf.org/doc/html/rfc7662 // https://datatracker.ietf.org/doc/html/rfc7662
// The recommended Response Data type is [oidc.IntrospectionResponse]. // The recommended Response Data type is [oidc.IntrospectionResponse].
Introspect(context.Context, *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) Introspect(context.Context, *Request[IntrospectionRequest]) (*Response, error)
// UserInfo handles the UserInfo endpoint and returns Claims about the authenticated End-User. // UserInfo handles the UserInfo endpoint and returns Claims about the authenticated End-User.
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
@ -329,7 +329,7 @@ func (UnimplementedServer) DeviceToken(ctx context.Context, r *ClientRequest[oid
return nil, unimplementedGrantError(oidc.GrantTypeDeviceCode) return nil, unimplementedGrantError(oidc.GrantTypeDeviceCode)
} }
func (UnimplementedServer) Introspect(ctx context.Context, r *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) { func (UnimplementedServer) Introspect(ctx context.Context, r *Request[IntrospectionRequest]) (*Response, error) {
return nil, unimplementedError(r) return nil, unimplementedError(r)
} }

View file

@ -25,9 +25,11 @@ func RegisterServer(server Server, endpoints Endpoints, options ...ServerOption)
decoder.IgnoreUnknownKeys(true) decoder.IgnoreUnknownKeys(true)
ws := &webServer{ ws := &webServer{
router: chi.NewRouter(),
server: server, server: server,
endpoints: endpoints, endpoints: endpoints,
decoder: decoder, decoder: decoder,
corsOpts: &defaultCORSOptions,
logger: slog.Default(), logger: slog.Default(),
} }
@ -36,6 +38,10 @@ func RegisterServer(server Server, endpoints Endpoints, options ...ServerOption)
} }
ws.createRouter() ws.createRouter()
ws.handler = ws.router
if ws.corsOpts != nil {
ws.handler = cors.New(*ws.corsOpts).Handler(ws.router)
}
return ws return ws
} }
@ -45,7 +51,14 @@ type ServerOption func(s *webServer)
// the Server's router. // the Server's router.
func WithHTTPMiddleware(m ...func(http.Handler) http.Handler) ServerOption { func WithHTTPMiddleware(m ...func(http.Handler) http.Handler) ServerOption {
return func(s *webServer) { return func(s *webServer) {
s.middleware = m s.router.Use(m...)
}
}
// WithSetRouter allows customization or the Server's router.
func WithSetRouter(set func(chi.Router)) ServerOption {
return func(s *webServer) {
set(s.router)
} }
} }
@ -57,6 +70,13 @@ func WithDecoder(decoder httphelper.Decoder) ServerOption {
} }
} }
// WithServerCORSOptions sets the CORS policy for the Server's router.
func WithServerCORSOptions(opts *cors.Options) ServerOption {
return func(s *webServer) {
s.corsOpts = opts
}
}
// WithFallbackLogger overrides the fallback logger, which // WithFallbackLogger overrides the fallback logger, which
// is used when no logger was found in the context. // is used when no logger was found in the context.
// Defaults to [slog.Default]. // Defaults to [slog.Default].
@ -67,14 +87,19 @@ func WithFallbackLogger(logger *slog.Logger) ServerOption {
} }
type webServer struct { type webServer struct {
http.Handler
server Server server Server
middleware []func(http.Handler) http.Handler router *chi.Mux
handler http.Handler
endpoints Endpoints endpoints Endpoints
decoder httphelper.Decoder decoder httphelper.Decoder
corsOpts *cors.Options
logger *slog.Logger logger *slog.Logger
} }
func (s *webServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.handler.ServeHTTP(w, r)
}
func (s *webServer) getLogger(ctx context.Context) *slog.Logger { func (s *webServer) getLogger(ctx context.Context) *slog.Logger {
if logger, ok := logging.FromContext(ctx); ok { if logger, ok := logging.FromContext(ctx); ok {
return logger return logger
@ -83,27 +108,23 @@ func (s *webServer) getLogger(ctx context.Context) *slog.Logger {
} }
func (s *webServer) createRouter() { func (s *webServer) createRouter() {
router := chi.NewRouter() s.router.HandleFunc(healthEndpoint, simpleHandler(s, s.server.Health))
router.Use(cors.New(defaultCORSOptions).Handler) s.router.HandleFunc(readinessEndpoint, simpleHandler(s, s.server.Ready))
router.Use(s.middleware...) s.router.HandleFunc(oidc.DiscoveryEndpoint, simpleHandler(s, s.server.Discovery))
router.HandleFunc(healthEndpoint, simpleHandler(s, s.server.Health))
router.HandleFunc(readinessEndpoint, simpleHandler(s, s.server.Ready))
router.HandleFunc(oidc.DiscoveryEndpoint, simpleHandler(s, s.server.Discovery))
s.endpointRoute(router, s.endpoints.Authorization, s.authorizeHandler) s.endpointRoute(s.endpoints.Authorization, s.authorizeHandler)
s.endpointRoute(router, s.endpoints.DeviceAuthorization, s.withClient(s.deviceAuthorizationHandler)) s.endpointRoute(s.endpoints.DeviceAuthorization, s.withClient(s.deviceAuthorizationHandler))
s.endpointRoute(router, s.endpoints.Token, s.tokensHandler) s.endpointRoute(s.endpoints.Token, s.tokensHandler)
s.endpointRoute(router, s.endpoints.Introspection, s.withClient(s.introspectionHandler)) s.endpointRoute(s.endpoints.Introspection, s.introspectionHandler)
s.endpointRoute(router, s.endpoints.Userinfo, s.userInfoHandler) s.endpointRoute(s.endpoints.Userinfo, s.userInfoHandler)
s.endpointRoute(router, s.endpoints.Revocation, s.withClient(s.revocationHandler)) s.endpointRoute(s.endpoints.Revocation, s.withClient(s.revocationHandler))
s.endpointRoute(router, s.endpoints.EndSession, s.endSessionHandler) s.endpointRoute(s.endpoints.EndSession, s.endSessionHandler)
s.endpointRoute(router, s.endpoints.JwksURI, simpleHandler(s, s.server.Keys)) s.endpointRoute(s.endpoints.JwksURI, simpleHandler(s, s.server.Keys))
s.Handler = router
} }
func (s *webServer) endpointRoute(router *chi.Mux, e *Endpoint, hf http.HandlerFunc) { func (s *webServer) endpointRoute(e *Endpoint, hf http.HandlerFunc) {
if e != nil { if e != nil {
router.HandleFunc(e.Relative(), hf) s.router.HandleFunc(e.Relative(), hf)
s.logger.Info("registered route", "endpoint", e.Relative()) s.logger.Info("registered route", "endpoint", e.Relative())
} }
} }
@ -128,7 +149,21 @@ func (s *webServer) withClient(handler clientHandler) http.HandlerFunc {
} }
func (s *webServer) verifyRequestClient(r *http.Request) (_ Client, err error) { func (s *webServer) verifyRequestClient(r *http.Request) (_ Client, err error) {
if err = r.ParseForm(); err != nil { cc, err := s.parseClientCredentials(r)
if err != nil {
return nil, err
}
return s.server.VerifyClient(r.Context(), &Request[ClientCredentials]{
Method: r.Method,
URL: r.URL,
Header: r.Header,
Form: r.Form,
Data: cc,
})
}
func (s *webServer) parseClientCredentials(r *http.Request) (_ *ClientCredentials, err error) {
if err := r.ParseForm(); err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err) return nil, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err)
} }
cc := new(ClientCredentials) cc := new(ClientCredentials)
@ -152,13 +187,7 @@ func (s *webServer) verifyRequestClient(r *http.Request) (_ Client, err error) {
if cc.ClientAssertion != "" && cc.ClientAssertionType != oidc.ClientAssertionTypeJWTAssertion { if cc.ClientAssertion != "" && cc.ClientAssertionType != oidc.ClientAssertionTypeJWTAssertion {
return nil, oidc.ErrInvalidRequest().WithDescription("invalid client_assertion_type %s", cc.ClientAssertionType) return nil, oidc.ErrInvalidRequest().WithDescription("invalid client_assertion_type %s", cc.ClientAssertionType)
} }
return s.server.VerifyClient(r.Context(), &Request[ClientCredentials]{ return cc, nil
Method: r.Method,
URL: r.URL,
Header: r.Header,
Form: r.Form,
Data: cc,
})
} }
func (s *webServer) authorizeHandler(w http.ResponseWriter, r *http.Request) { func (s *webServer) authorizeHandler(w http.ResponseWriter, r *http.Request) {
@ -370,8 +399,13 @@ func (s *webServer) deviceTokenHandler(w http.ResponseWriter, r *http.Request, c
resp.writeOut(w) resp.writeOut(w)
} }
func (s *webServer) introspectionHandler(w http.ResponseWriter, r *http.Request, client Client) { func (s *webServer) introspectionHandler(w http.ResponseWriter, r *http.Request) {
if client.AuthMethod() == oidc.AuthMethodNone { cc, err := s.parseClientCredentials(r)
if err != nil {
WriteError(w, r, err, s.getLogger(r.Context()))
return
}
if cc.ClientSecret == "" && cc.ClientAssertion == "" {
WriteError(w, r, oidc.ErrInvalidClient().WithDescription("client must be authenticated"), s.getLogger(r.Context())) WriteError(w, r, oidc.ErrInvalidClient().WithDescription("client must be authenticated"), s.getLogger(r.Context()))
return return
} }
@ -384,7 +418,7 @@ func (s *webServer) introspectionHandler(w http.ResponseWriter, r *http.Request,
WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("token missing"), s.getLogger(r.Context())) WriteError(w, r, oidc.ErrInvalidRequest().WithDescription("token missing"), s.getLogger(r.Context()))
return return
} }
resp, err := s.server.Introspect(r.Context(), newClientRequest(r, request, client)) resp, err := s.server.Introspect(r.Context(), newRequest(r, &IntrospectionRequest{cc, request}))
if err != nil { if err != nil {
WriteError(w, r, err, s.getLogger(r.Context())) WriteError(w, r, err, s.getLogger(r.Context()))
return return

View file

@ -32,7 +32,7 @@ func jwtProfile() (string, error) {
} }
func TestServerRoutes(t *testing.T) { func TestServerRoutes(t *testing.T) {
server := op.NewLegacyServer(testProvider, *op.DefaultEndpoints) server := op.RegisterLegacyServer(op.NewLegacyServer(testProvider, *op.DefaultEndpoints))
storage := testProvider.Storage().(routesTestStorage) storage := testProvider.Storage().(routesTestStorage)
ctx := op.ContextWithIssuer(context.Background(), testIssuer) ctx := op.ContextWithIssuer(context.Background(), testIssuer)

View file

@ -365,14 +365,14 @@ func Test_webServer_authorizeHandler(t *testing.T) {
}, },
}, },
{ {
name: "authorize error", name: "server error",
fields: fields{ fields: fields{
server: &requestVerifier{}, server: &requestVerifier{},
decoder: testDecoder, decoder: testDecoder,
}, },
r: httptest.NewRequest(http.MethodPost, "/authorize", strings.NewReader("foo=bar")), r: httptest.NewRequest(http.MethodPost, "/authorize", strings.NewReader("foo=bar")),
want: webServerResult{ want: webServerResult{
wantStatus: http.StatusBadRequest, wantStatus: http.StatusInternalServerError,
wantBody: `{"error":"server_error"}`, wantBody: `{"error":"server_error"}`,
}, },
}, },
@ -1001,14 +1001,12 @@ func Test_webServer_introspectionHandler(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
decoder httphelper.Decoder decoder httphelper.Decoder
client Client
r *http.Request r *http.Request
want webServerResult want webServerResult
}{ }{
{ {
name: "decoder error", name: "decoder error",
decoder: schema.NewDecoder(), decoder: schema.NewDecoder(),
client: newClient(clientTypeUserAgent),
r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")), r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")),
want: webServerResult{ want: webServerResult{
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
@ -1018,8 +1016,7 @@ func Test_webServer_introspectionHandler(t *testing.T) {
{ {
name: "public client", name: "public client",
decoder: testDecoder, decoder: testDecoder,
client: newClient(clientTypeNative), r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("client_id=123")),
r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")),
want: webServerResult{ want: webServerResult{
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantBody: `{"error":"invalid_client", "error_description":"client must be authenticated"}`, wantBody: `{"error":"invalid_client", "error_description":"client must be authenticated"}`,
@ -1028,8 +1025,7 @@ func Test_webServer_introspectionHandler(t *testing.T) {
{ {
name: "token missing", name: "token missing",
decoder: testDecoder, decoder: testDecoder,
client: newClient(clientTypeWeb), r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("client_id=123&client_secret=SECRET")),
r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar")),
want: webServerResult{ want: webServerResult{
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantBody: `{"error":"invalid_request", "error_description":"token missing"}`, wantBody: `{"error":"invalid_request", "error_description":"token missing"}`,
@ -1038,8 +1034,7 @@ func Test_webServer_introspectionHandler(t *testing.T) {
{ {
name: "unimplemented Introspect called", name: "unimplemented Introspect called",
decoder: testDecoder, decoder: testDecoder,
client: newClient(clientTypeWeb), r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("client_id=123&client_secret=SECRET&token=xxx")),
r: httptest.NewRequest(http.MethodPost, "/", strings.NewReader("token=xxx")),
want: webServerResult{ want: webServerResult{
wantStatus: UnimplementedStatusCode, wantStatus: UnimplementedStatusCode,
wantBody: `{"error":"server_error", "error_description":"/ not implemented on this server"}`, wantBody: `{"error":"server_error", "error_description":"/ not implemented on this server"}`,
@ -1053,7 +1048,7 @@ func Test_webServer_introspectionHandler(t *testing.T) {
decoder: tt.decoder, decoder: tt.decoder,
logger: slog.Default(), logger: slog.Default(),
} }
runWebServerClientTest(t, s.introspectionHandler, tt.r, tt.client, tt.want) runWebServerTest(t, s.introspectionHandler, tt.r, tt.want)
}) })
} }
} }
@ -1242,7 +1237,7 @@ func Test_webServer_simpleHandler(t *testing.T) {
}, },
r: httptest.NewRequest(http.MethodGet, "/", bytes.NewReader(make([]byte, 11<<20))), r: httptest.NewRequest(http.MethodGet, "/", bytes.NewReader(make([]byte, 11<<20))),
want: webServerResult{ want: webServerResult{
wantStatus: http.StatusBadRequest, wantStatus: http.StatusInternalServerError,
wantBody: `{"error":"server_error", "error_description":"io: read/write on closed pipe"}`, wantBody: `{"error":"server_error", "error_description":"io: read/write on closed pipe"}`,
}, },
}, },

View file

@ -10,37 +10,77 @@ import (
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
) )
// LegacyServer is an implementation of [Server[] that // ExtendedLegacyServer allows embedding [LegacyServer] in a struct,
// simply wraps a [OpenIDProvider]. // so that its methods can be individually overridden.
//
// EXPERIMENTAL: may change until v4
type ExtendedLegacyServer interface {
Server
Provider() OpenIDProvider
Endpoints() Endpoints
AuthCallbackURL() func(context.Context, string) string
}
// RegisterLegacyServer registers a [LegacyServer] or an extension thereof.
// It takes care of registering the IssuerFromRequest middleware
// and Authorization Callback Routes.
// Neither are part of the bare [Server] interface.
//
// EXPERIMENTAL: may change until v4
func RegisterLegacyServer(s ExtendedLegacyServer, options ...ServerOption) http.Handler {
provider := s.Provider()
options = append(options,
WithHTTPMiddleware(intercept(provider.IssuerFromRequest)),
WithSetRouter(func(r chi.Router) {
r.HandleFunc(s.Endpoints().Authorization.Relative()+authCallbackPathSuffix, authorizeCallbackHandler(provider))
}),
)
return RegisterServer(s, s.Endpoints(), options...)
}
// LegacyServer is an implementation of [Server] that
// simply wraps an [OpenIDProvider].
// It can be used to transition from the former Provider/Storage // It can be used to transition from the former Provider/Storage
// interfaces to the new Server interface. // interfaces to the new Server interface.
//
// EXPERIMENTAL: may change until v4
type LegacyServer struct { type LegacyServer struct {
UnimplementedServer UnimplementedServer
provider OpenIDProvider provider OpenIDProvider
endpoints Endpoints endpoints Endpoints
} }
// NewLegacyServer wraps provider in a `Server` and returns a handler which is // NewLegacyServer wraps provider in a `Server` implementation
// the Server's router.
// //
// Only non-nil endpoints will be registered on the router. // Only non-nil endpoints will be registered on the router.
// Nil endpoints are disabled. // Nil endpoints are disabled.
// //
// The passed endpoints is also set to the provider, // The passed endpoints is also used for the discovery config,
// to be consistent with the discovery config. // and endpoints already set to the provider are ignored.
// Any `With*Endpoint()` option used on the provider is // Any `With*Endpoint()` option used on the provider is
// therefore ineffective. // therefore ineffective.
func NewLegacyServer(provider OpenIDProvider, endpoints Endpoints) http.Handler { //
server := RegisterServer(&LegacyServer{ // EXPERIMENTAL: may change until v4
func NewLegacyServer(provider OpenIDProvider, endpoints Endpoints) *LegacyServer {
return &LegacyServer{
provider: provider, provider: provider,
endpoints: endpoints, endpoints: endpoints,
}, endpoints, WithHTTPMiddleware(intercept(provider.IssuerFromRequest))) }
}
router := chi.NewRouter() func (s *LegacyServer) Provider() OpenIDProvider {
router.Mount("/", server) return s.provider
router.HandleFunc(authCallbackPath(provider), authorizeCallbackHandler(provider)) }
return router func (s *LegacyServer) Endpoints() Endpoints {
return s.endpoints
}
// AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login
func (s *LegacyServer) AuthCallbackURL() func(context.Context, string) string {
return func(ctx context.Context, requestID string) string {
return s.endpoints.Authorization.Absolute(IssuerFromContext(ctx)) + authCallbackPathSuffix + "?id=" + requestID
}
} }
func (s *LegacyServer) Health(_ context.Context, r *Request[struct{}]) (*Response, error) { func (s *LegacyServer) Health(_ context.Context, r *Request[struct{}]) (*Response, error) {
@ -51,7 +91,7 @@ func (s *LegacyServer) Ready(ctx context.Context, r *Request[struct{}]) (*Respon
for _, probe := range s.provider.Probes() { for _, probe := range s.provider.Probes() {
// shouldn't we run probes in Go routines? // shouldn't we run probes in Go routines?
if err := probe(ctx); err != nil { if err := probe(ctx); err != nil {
return nil, NewStatusError(err, http.StatusInternalServerError) return nil, AsStatusError(err, http.StatusInternalServerError)
} }
} }
return NewResponse(Status{Status: "ok"}), nil return NewResponse(Status{Status: "ok"}), nil
@ -66,7 +106,7 @@ func (s *LegacyServer) Discovery(ctx context.Context, r *Request[struct{}]) (*Re
func (s *LegacyServer) Keys(ctx context.Context, r *Request[struct{}]) (*Response, error) { func (s *LegacyServer) Keys(ctx context.Context, r *Request[struct{}]) (*Response, error) {
keys, err := s.provider.Storage().KeySet(ctx) keys, err := s.provider.Storage().KeySet(ctx)
if err != nil { if err != nil {
return nil, NewStatusError(err, http.StatusInternalServerError) return nil, AsStatusError(err, http.StatusInternalServerError)
} }
return NewResponse(jsonWebKeySet(keys)), nil return NewResponse(jsonWebKeySet(keys)), nil
} }
@ -87,7 +127,7 @@ func (s *LegacyServer) VerifyAuthRequest(ctx context.Context, r *Request[oidc.Au
} }
} }
if r.Data.ClientID == "" { if r.Data.ClientID == "" {
return nil, ErrAuthReqMissingClientID return nil, oidc.ErrInvalidRequest().WithParent(ErrAuthReqMissingClientID).WithDescription(ErrAuthReqMissingClientID.Error())
} }
client, err := s.provider.Storage().GetClientByClientID(ctx, r.Data.ClientID) client, err := s.provider.Storage().GetClientByClientID(ctx, r.Data.ClientID)
if err != nil { if err != nil {
@ -115,7 +155,7 @@ func (s *LegacyServer) Authorize(ctx context.Context, r *ClientRequest[oidc.Auth
func (s *LegacyServer) DeviceAuthorization(ctx context.Context, r *ClientRequest[oidc.DeviceAuthorizationRequest]) (*Response, error) { func (s *LegacyServer) DeviceAuthorization(ctx context.Context, r *ClientRequest[oidc.DeviceAuthorizationRequest]) (*Response, error) {
response, err := createDeviceAuthorization(ctx, r.Data, r.Client.GetID(), s.provider) response, err := createDeviceAuthorization(ctx, r.Data, r.Client.GetID(), s.provider)
if err != nil { if err != nil {
return nil, NewStatusError(err, http.StatusInternalServerError) return nil, AsStatusError(err, http.StatusInternalServerError)
} }
return NewResponse(response), nil return NewResponse(response), nil
} }
@ -165,11 +205,14 @@ func (s *LegacyServer) CodeExchange(ctx context.Context, r *ClientRequest[oidc.A
if err != nil { if err != nil {
return nil, err return nil, err
} }
if r.Client.AuthMethod() == oidc.AuthMethodNone { if r.Client.AuthMethod() == oidc.AuthMethodNone || r.Data.CodeVerifier != "" {
if err = AuthorizeCodeChallenge(r.Data.CodeVerifier, authReq.GetCodeChallenge()); err != nil { if err = AuthorizeCodeChallenge(r.Data.CodeVerifier, authReq.GetCodeChallenge()); err != nil {
return nil, err return nil, err
} }
} }
if r.Data.RedirectURI != authReq.GetRedirectURI() {
return nil, oidc.ErrInvalidGrant().WithDescription("redirect_uri does not correspond")
}
resp, err := CreateTokenResponse(ctx, authReq, r.Client, s.provider, true, r.Data.Code, "") resp, err := CreateTokenResponse(ctx, authReq, r.Client, s.provider, true, r.Data.Code, "")
if err != nil { if err != nil {
return nil, err return nil, err
@ -205,7 +248,7 @@ func (s *LegacyServer) JWTProfile(ctx context.Context, r *Request[oidc.JWTProfil
} }
tokenRequest, err := VerifyJWTAssertion(ctx, r.Data.Assertion, exchanger.JWTProfileVerifier(ctx)) tokenRequest, err := VerifyJWTAssertion(ctx, r.Data.Assertion, exchanger.JWTProfileVerifier(ctx))
if err != nil { if err != nil {
return nil, err return nil, oidc.ErrInvalidRequest().WithParent(err).WithDescription("assertion invalid")
} }
tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(ctx, tokenRequest.Issuer, r.Data.Scope) tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(ctx, tokenRequest.Issuer, r.Data.Scope)
@ -251,7 +294,7 @@ func (s *LegacyServer) ClientCredentialsExchange(ctx context.Context, r *ClientR
} }
func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error) { func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error) {
if !s.provider.GrantTypeClientCredentialsSupported() { if !s.provider.GrantTypeDeviceCodeSupported() {
return nil, unimplementedGrantError(oidc.GrantTypeDeviceCode) return nil, unimplementedGrantError(oidc.GrantTypeDeviceCode)
} }
// use a limited context timeout shorter as the default // use a limited context timeout shorter as the default
@ -259,15 +302,10 @@ func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.De
ctx, cancel := context.WithTimeout(ctx, 4*time.Second) ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
defer cancel() defer cancel()
state, err := CheckDeviceAuthorizationState(ctx, r.Client.GetID(), r.Data.DeviceCode, s.provider) tokenRequest, err := CheckDeviceAuthorizationState(ctx, r.Client.GetID(), r.Data.DeviceCode, s.provider)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tokenRequest := &deviceAccessTokenRequest{
subject: state.Subject,
audience: []string{r.Client.GetID()},
scopes: state.Scopes,
}
resp, err := CreateDeviceTokenResponse(ctx, tokenRequest, s.provider, r.Client) resp, err := CreateDeviceTokenResponse(ctx, tokenRequest, s.provider, r.Client)
if err != nil { if err != nil {
return nil, err return nil, err
@ -275,13 +313,30 @@ func (s *LegacyServer) DeviceToken(ctx context.Context, r *ClientRequest[oidc.De
return NewResponse(resp), nil return NewResponse(resp), nil
} }
func (s *LegacyServer) Introspect(ctx context.Context, r *ClientRequest[oidc.IntrospectionRequest]) (*Response, error) { func (s *LegacyServer) authenticateResourceClient(ctx context.Context, cc *ClientCredentials) (string, error) {
if cc.ClientAssertion != "" {
if jp, ok := s.provider.(ClientJWTProfile); ok {
return ClientJWTAuth(ctx, oidc.ClientAssertionParams{ClientAssertion: cc.ClientAssertion}, jp)
}
return "", oidc.ErrInvalidClient().WithDescription("client_assertion not supported")
}
if err := s.provider.Storage().AuthorizeClientIDSecret(ctx, cc.ClientID, cc.ClientSecret); err != nil {
return "", oidc.ErrUnauthorizedClient().WithParent(err)
}
return cc.ClientID, nil
}
func (s *LegacyServer) Introspect(ctx context.Context, r *Request[IntrospectionRequest]) (*Response, error) {
clientID, err := s.authenticateResourceClient(ctx, r.Data.ClientCredentials)
if err != nil {
return nil, err
}
response := new(oidc.IntrospectionResponse) response := new(oidc.IntrospectionResponse)
tokenID, subject, ok := getTokenIDAndSubject(ctx, s.provider, r.Data.Token) tokenID, subject, ok := getTokenIDAndSubject(ctx, s.provider, r.Data.Token)
if !ok { if !ok {
return NewResponse(response), nil return NewResponse(response), nil
} }
err := s.provider.Storage().SetIntrospectionFromToken(ctx, response, tokenID, subject, r.Client.GetID()) err = s.provider.Storage().SetIntrospectionFromToken(ctx, response, tokenID, subject, clientID)
if err != nil { if err != nil {
return NewResponse(response), nil return NewResponse(response), nil
} }
@ -336,9 +391,14 @@ func (s *LegacyServer) EndSession(ctx context.Context, r *Request[oidc.EndSessio
if err != nil { if err != nil {
return nil, err return nil, err
} }
redirect := session.RedirectURI
if fromRequest, ok := s.provider.Storage().(CanTerminateSessionFromRequest); ok {
redirect, err = fromRequest.TerminateSessionFromRequest(ctx, session)
} else {
err = s.provider.Storage().TerminateSession(ctx, session.UserID, session.ClientID) err = s.provider.Storage().TerminateSession(ctx, session.UserID, session.ClientID)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewRedirect(session.RedirectURI), nil return NewRedirect(redirect), nil
} }

View file

@ -2,6 +2,7 @@ package op
import ( import (
"context" "context"
"errors"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
@ -68,7 +69,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest,
} }
if req.IdTokenHint != "" { if req.IdTokenHint != "" {
claims, err := VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx)) claims, err := VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx))
if err != nil { if err != nil && !errors.As(err, &IDTokenHintExpiredError{}) {
return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err) return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err)
} }
session.UserID = claims.GetSubject() session.UserID = claims.GetSubject()

View file

@ -168,15 +168,6 @@ type EndSessionRequest struct {
var ErrDuplicateUserCode = errors.New("user code already exists") var ErrDuplicateUserCode = errors.New("user code already exists")
type DeviceAuthorizationState struct {
ClientID string
Scopes []string
Expires time.Time
Done bool
Subject string
Denied bool
}
type DeviceAuthorizationStorage interface { type DeviceAuthorizationStorage interface {
// StoreDeviceAuthorizationRequest stores a new device authorization request in the database. // StoreDeviceAuthorizationRequest stores a new device authorization request in the database.
// User code will be used by the user to complete the login flow and must be unique. // User code will be used by the user to complete the login flow and must be unique.

View file

@ -84,6 +84,8 @@ func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool
return req.GetRequestedTokenType() == oidc.RefreshTokenType return req.GetRequestedTokenType() == oidc.RefreshTokenType
case RefreshTokenRequest: case RefreshTokenRequest:
return true return true
case *DeviceAuthorizationState:
return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && ValidateGrantType(client, oidc.GrantTypeRefreshToken)
default: default:
return false return false
} }

View file

@ -65,3 +65,8 @@ func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector)
return req.Token, clientID, nil return req.Token, clientID, nil
} }
type IntrospectionRequest struct {
*ClientCredentials
*oidc.IntrospectionRequest
}

View file

@ -2,6 +2,7 @@ package op
import ( import (
"context" "context"
"errors"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
) )
@ -27,8 +28,23 @@ func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet, opts ...IDTokenHi
return verifier return verifier
} }
type IDTokenHintExpiredError struct {
error
}
func (e IDTokenHintExpiredError) Unwrap() error {
return e.error
}
func (e IDTokenHintExpiredError) Is(err error) bool {
return errors.Is(err, e.error)
}
// VerifyIDTokenHint validates the id token according to // VerifyIDTokenHint 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.
// In case of an expired token both the Claims and first encountered expiry related error
// is returned of type [IDTokenHintExpiredError]. In that case the caller can choose to still
// trust the token for cases like logout, as signature and other verifications succeeded.
func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v *IDTokenHintVerifier) (claims C, err error) { func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v *IDTokenHintVerifier) (claims C, err error) {
var nilClaims C var nilClaims C
@ -49,20 +65,20 @@ func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v *IDTo
return nilClaims, err return nilClaims, err
} }
if err = oidc.CheckExpiration(claims, v.Offset); err != nil {
return nilClaims, err
}
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil {
return nilClaims, err
}
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil { if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR); err != nil {
return nilClaims, err return nilClaims, err
} }
if err = oidc.CheckExpiration(claims, v.Offset); err != nil {
return claims, IDTokenHintExpiredError{err}
}
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT, v.Offset); err != nil {
return claims, IDTokenHintExpiredError{err}
}
if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil { if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil {
return nilClaims, err return claims, IDTokenHintExpiredError{err}
} }
return claims, nil return claims, nil
} }

View file

@ -2,6 +2,7 @@ package op
import ( import (
"context" "context"
"errors"
"testing" "testing"
"time" "time"
@ -57,6 +58,13 @@ func TestNewIDTokenHintVerifier(t *testing.T) {
} }
} }
func Test_IDTokenHintExpiredError(t *testing.T) {
var err error = IDTokenHintExpiredError{oidc.ErrExpired}
assert.True(t, errors.Unwrap(err) == oidc.ErrExpired)
assert.ErrorIs(t, err, oidc.ErrExpired)
assert.ErrorAs(t, err, &IDTokenHintExpiredError{})
}
func TestVerifyIDTokenHint(t *testing.T) { func TestVerifyIDTokenHint(t *testing.T) {
verifier := &IDTokenHintVerifier{ verifier := &IDTokenHintVerifier{
Issuer: tu.ValidIssuer, Issuer: tu.ValidIssuer,
@ -71,21 +79,23 @@ func TestVerifyIDTokenHint(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
tokenClaims func() (string, *oidc.IDTokenClaims) tokenClaims func() (string, *oidc.IDTokenClaims)
wantErr bool wantClaims bool
wantErr error
}{ }{
{ {
name: "success", name: "success",
tokenClaims: tu.ValidIDToken, tokenClaims: tu.ValidIDToken,
wantClaims: true,
}, },
{ {
name: "parse err", name: "parse err",
tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil }, tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil },
wantErr: true, wantErr: oidc.ErrParse,
}, },
{ {
name: "invalid signature", name: "invalid signature",
tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil }, tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil },
wantErr: true, wantErr: oidc.ErrSignatureUnsupportedAlg,
}, },
{ {
name: "wrong issuer", name: "wrong issuer",
@ -96,29 +106,7 @@ func TestVerifyIDTokenHint(t *testing.T) {
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
) )
}, },
wantErr: true, wantErr: oidc.ErrIssuerInvalid,
},
{
name: "expired",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantErr: true,
},
{
name: "wrong IAT",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, -time.Hour, "",
)
},
wantErr: true,
}, },
{ {
name: "wrong acr", name: "wrong acr",
@ -129,7 +117,31 @@ func TestVerifyIDTokenHint(t *testing.T) {
"else", tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", "else", tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
) )
}, },
wantErr: true, wantErr: oidc.ErrAcrInvalid,
},
{
name: "expired",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
)
},
wantClaims: true,
wantErr: IDTokenHintExpiredError{oidc.ErrExpired},
},
{
name: "IAT too old",
tokenClaims: func() (string, *oidc.IDTokenClaims) {
return tu.NewIDToken(
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, time.Hour, "",
)
},
wantClaims: true,
wantErr: IDTokenHintExpiredError{oidc.ErrIatToOld},
}, },
{ {
name: "expired auth", name: "expired auth",
@ -140,7 +152,8 @@ func TestVerifyIDTokenHint(t *testing.T) {
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "", tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
) )
}, },
wantErr: true, wantClaims: true,
wantErr: IDTokenHintExpiredError{oidc.ErrAuthTimeToOld},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -148,14 +161,12 @@ func TestVerifyIDTokenHint(t *testing.T) {
token, want := tt.tokenClaims() token, want := tt.tokenClaims()
got, err := VerifyIDTokenHint[*oidc.IDTokenClaims](context.Background(), token, verifier) got, err := VerifyIDTokenHint[*oidc.IDTokenClaims](context.Background(), token, verifier)
if tt.wantErr { require.ErrorIs(t, err, tt.wantErr)
assert.Error(t, err) if tt.wantClaims {
assert.Nil(t, got) assert.Equal(t, got, want, "claims")
return return
} }
require.NoError(t, err) assert.Nil(t, got, "claims")
require.NotNil(t, got)
assert.Equal(t, got, want)
}) })
} }
} }

View file

@ -17,11 +17,21 @@ import (
type JWTProfileVerifier struct { type JWTProfileVerifier struct {
oidc.Verifier oidc.Verifier
Storage JWTProfileKeyStorage Storage JWTProfileKeyStorage
keySet oidc.KeySet
CheckSubject func(request *oidc.JWTTokenRequest) error CheckSubject func(request *oidc.JWTTokenRequest) error
} }
// NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication) // NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication)
func NewJWTProfileVerifier(storage JWTProfileKeyStorage, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) *JWTProfileVerifier { func NewJWTProfileVerifier(storage JWTProfileKeyStorage, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) *JWTProfileVerifier {
return newJWTProfileVerifier(storage, nil, issuer, maxAgeIAT, offset, opts...)
}
// NewJWTProfileVerifierKeySet creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication)
func NewJWTProfileVerifierKeySet(keySet oidc.KeySet, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) *JWTProfileVerifier {
return newJWTProfileVerifier(nil, keySet, issuer, maxAgeIAT, offset, opts...)
}
func newJWTProfileVerifier(storage JWTProfileKeyStorage, keySet oidc.KeySet, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) *JWTProfileVerifier {
j := &JWTProfileVerifier{ j := &JWTProfileVerifier{
Verifier: oidc.Verifier{ Verifier: oidc.Verifier{
Issuer: issuer, Issuer: issuer,
@ -29,6 +39,7 @@ func NewJWTProfileVerifier(storage JWTProfileKeyStorage, issuer string, maxAgeIA
Offset: offset, Offset: offset,
}, },
Storage: storage, Storage: storage,
keySet: keySet,
CheckSubject: SubjectIsIssuer, CheckSubject: SubjectIsIssuer,
} }
@ -78,7 +89,10 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v *JWTProfileVeri
return nil, err return nil, err
} }
keySet := &jwtProfileKeySet{storage: v.Storage, clientID: request.Issuer} keySet := v.keySet
if keySet == nil {
keySet = &jwtProfileKeySet{storage: v.Storage, clientID: request.Issuer}
}
if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil {
return nil, err return nil, err
} }