Merge branch 'main' into fix-empty-locale

This commit is contained in:
Tim Möhlmann 2025-03-12 12:54:09 +02:00 committed by GitHub
commit 7dcd1ec03c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 284 additions and 143 deletions

View file

@ -18,7 +18,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
go: ['1.21', '1.22'] go: ['1.21', '1.22', '1.23']
name: Go ${{ matrix.go }} test name: Go ${{ matrix.go }} test
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -27,7 +27,7 @@ jobs:
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@v4.6.0 - uses: codecov/codecov-action@v5.4.0
with: with:
file: ./profile.cov file: ./profile.cov
name: codecov-go name: codecov-go

View file

@ -21,6 +21,7 @@ Whenever possible we tried to reuse / extend existing packages like `OAuth2 for
## Basic Overview ## Basic Overview
The most important packages of the library: The most important packages of the library:
<pre> <pre>
/pkg /pkg
/client clients using the OP for retrieving, exchanging and verifying tokens /client clients using the OP for retrieving, exchanging and verifying tokens
@ -37,7 +38,6 @@ The most important packages of the library:
/server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI /server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI
</pre> </pre>
### Semver ### Semver
This package uses [semver](https://semver.org/) for [releases](https://github.com/zitadel/oidc/releases). Major releases ship breaking changes. Starting with the `v2` to `v3` increment we provide an [upgrade guide](UPGRADING.md) to ease migration to a newer version. This package uses [semver](https://semver.org/) for [releases](https://github.com/zitadel/oidc/releases). Major releases ship breaking changes. Starting with the `v2` to `v3` increment we provide an [upgrade guide](UPGRADING.md) to ease migration to a newer version.
@ -60,10 +60,13 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid
- the OP will redirect you to the client app, which displays the user info - the OP will redirect you to the client app, which displays the user info
for the dynamic issuer, just start it with: for the dynamic issuer, just start it with:
```bash ```bash
go run github.com/zitadel/oidc/v3/example/server/dynamic go run github.com/zitadel/oidc/v3/example/server/dynamic
``` ```
the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with: the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with:
```bash ```bash
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app
``` ```
@ -76,12 +79,13 @@ Example server allows extra configuration using environment variables and could
end testing of your services. end testing of your services.
| Name | Format | Description | | Name | Format | Description |
|---------------|--------------------------------------|---------------------------------------| | ------------ | -------------------------------- | ------------------------------------- |
| PORT | Number between 1 and 65535 | OIDC listen port | | PORT | Number between 1 and 65535 | OIDC listen port |
| REDIRECT_URI | Comma-separated URIs | List of allowed redirect URIs | | REDIRECT_URI | Comma-separated URIs | List of allowed redirect URIs |
| USERS_FILE | Path to json in local filesystem | Users with their data and credentials | | USERS_FILE | Path to json in local filesystem | Users with their data and credentials |
Here is json equivalent for one of the default users Here is json equivalent for one of the default users
```json ```json
{ {
"id2": { "id2": {
@ -103,7 +107,7 @@ Here is json equivalent for one of the default users
## Features ## Features
| | Relying party | OpenID Provider | Specification | | | Relying party | OpenID Provider | Specification |
|----------------------| ------------- | --------------- |----------------------------------------------| | -------------------- | ------------- | --------------- | -------------------------------------------- |
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] | | Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] |
| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] | | Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] |
| Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] | | Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] |
@ -117,18 +121,18 @@ Here is json equivalent for one of the default users
| mTLS | not yet | not yet | [RFC 8705][11] | | mTLS | not yet | not yet | [RFC 8705][11] |
| Back-Channel Logout | not yet | yes | OpenID Connect [Back-Channel Logout][12] 1.0 | | Back-Channel Logout | not yet | yes | OpenID Connect [Back-Channel Logout][12] 1.0 |
[1]: <https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth> "3.1. Authentication using the Authorization Code Flow" [1]: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth "3.1. Authentication using the Authorization Code Flow"
[2]: <https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth> "3.2. Authentication using the Implicit Flow" [2]: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth "3.2. Authentication using the Implicit Flow"
[3]: <https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth> "3.3. Authentication using the Hybrid Flow" [3]: https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth "3.3. Authentication using the Hybrid Flow"
[4]: <https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication> "9. Client Authentication" [4]: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication "9. Client Authentication"
[5]: <https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens> "12. Using Refresh Tokens" [5]: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens "12. Using Refresh Tokens"
[6]: <https://openid.net/specs/openid-connect-discovery-1_0.html> "OpenID Connect Discovery 1.0 incorporating errata set 1" [6]: https://openid.net/specs/openid-connect-discovery-1_0.html "OpenID Connect Discovery 1.0 incorporating errata set 1"
[7]: <https://www.rfc-editor.org/rfc/rfc7523.html> "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants" [7]: https://www.rfc-editor.org/rfc/rfc7523.html "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
[8]: <https://www.rfc-editor.org/rfc/rfc7636.html> "Proof Key for Code Exchange by OAuth Public Clients" [8]: https://www.rfc-editor.org/rfc/rfc7636.html "Proof Key for Code Exchange by OAuth Public Clients"
[9]: <https://www.rfc-editor.org/rfc/rfc8693.html> "OAuth 2.0 Token Exchange" [9]: https://www.rfc-editor.org/rfc/rfc8693.html "OAuth 2.0 Token Exchange"
[10]: <https://www.rfc-editor.org/rfc/rfc8628.html> "OAuth 2.0 Device Authorization Grant" [10]: https://www.rfc-editor.org/rfc/rfc8628.html "OAuth 2.0 Device Authorization Grant"
[11]: <https://www.rfc-editor.org/rfc/rfc8705.html> "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens" [11]: https://www.rfc-editor.org/rfc/rfc8705.html "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
[12]: <https://openid.net/specs/openid-connect-backchannel-1_0.html> "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1" [12]: https://openid.net/specs/openid-connect-backchannel-1_0.html "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1"
## Contributors ## Contributors
@ -153,8 +157,9 @@ Versions that also build are marked with :warning:.
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| <1.21 | :x: | | <1.21 | :x: |
| 1.21 | :white_check_mark: | | 1.21 | :warning: |
| 1.22 | :white_check_mark: | | 1.22 | :white_check_mark: |
| 1.23 | :white_check_mark: |
## Why another library ## Why another library
@ -185,5 +190,4 @@ Unless required by applicable law or agreed to in writing, software distributed
AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License. language governing permissions and limitations under the License.
[^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892 [^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892

View file

@ -32,8 +32,8 @@ func main() {
storage.RegisterClients( storage.RegisterClients(
storage.NativeClient("native", cfg.RedirectURI...), storage.NativeClient("native", cfg.RedirectURI...),
storage.WebClient("web", "secret"), storage.WebClient("web", "secret", cfg.RedirectURI...),
storage.WebClient("api", "secret"), storage.WebClient("api", "secret", cfg.RedirectURI...),
) )
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations // the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations

View file

@ -164,6 +164,15 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques
} }
} }
type AuthRequestWithSessionState struct {
*AuthRequest
SessionState string
}
func (a *AuthRequestWithSessionState) GetSessionState() string {
return a.SessionState
}
type OIDCCodeChallenge struct { type OIDCCodeChallenge struct {
Challenge string Challenge string
Method string Method string

View file

@ -151,6 +151,9 @@ func (s *Storage) CheckUsernamePassword(username, password, id string) error {
// in this example we'll simply check the username / password and set a boolean to true // in this example we'll simply check the username / password and set a boolean to true
// therefore we will also just check this boolean if the request / login has been finished // therefore we will also just check this boolean if the request / login has been finished
request.done = true request.done = true
request.authTime = time.Now()
return nil return nil
} }
return fmt.Errorf("username or password wrong") return fmt.Errorf("username or password wrong")
@ -295,15 +298,19 @@ func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.T
// if we get here, the currentRefreshToken was not empty, so the call is a refresh token request // if we get here, the currentRefreshToken was not empty, so the call is a refresh token request
// we therefore will have to check the currentRefreshToken and renew the refresh token // we therefore will have to check the currentRefreshToken and renew the refresh token
refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken)
newRefreshToken = uuid.NewString()
accessToken, err := s.accessToken(applicationID, newRefreshToken, request.GetSubject(), request.GetAudience(), request.GetScopes())
if err != nil { if err != nil {
return "", "", time.Time{}, err return "", "", time.Time{}, err
} }
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
if err != nil { if err := s.renewRefreshToken(currentRefreshToken, newRefreshToken, accessToken.ID); err != nil {
return "", "", time.Time{}, err return "", "", time.Time{}, err
} }
return accessToken.ID, refreshToken, accessToken.Expiration, nil
return accessToken.ID, newRefreshToken, accessToken.Expiration, nil
} }
func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
@ -385,14 +392,9 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID
if refreshToken.ApplicationID != clientID { if refreshToken.ApplicationID != clientID {
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
} }
// if it is a refresh token, you will have to remove the access token as well
delete(s.refreshTokens, refreshToken.ID) delete(s.refreshTokens, refreshToken.ID)
for _, accessToken := range s.tokens { // if it is a refresh token, you will have to remove the access token as well
if accessToken.RefreshTokenID == refreshToken.ID { delete(s.tokens, refreshToken.AccessToken)
delete(s.tokens, accessToken.ID)
return nil
}
}
return nil return nil
} }
@ -488,6 +490,9 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserI
// return err // return err
// } // }
//} //}
if token.Expiration.Before(time.Now()) {
return fmt.Errorf("token is expired")
}
return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes) return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes)
} }
@ -594,33 +599,41 @@ func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime
Audience: accessToken.Audience, Audience: accessToken.Audience,
Expiration: time.Now().Add(5 * time.Hour), Expiration: time.Now().Add(5 * time.Hour),
Scopes: accessToken.Scopes, Scopes: accessToken.Scopes,
AccessToken: accessToken.ID,
} }
s.refreshTokens[token.ID] = token s.refreshTokens[token.ID] = token
return token.Token, nil return token.Token, nil
} }
// renewRefreshToken checks the provided refresh_token and creates a new one based on the current // renewRefreshToken checks the provided refresh_token and creates a new one based on the current
func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) { //
// [Refresh Token Rotation] is implemented.
//
// [Refresh Token Rotation]: https://www.rfc-editor.org/rfc/rfc6819#section-5.2.2.3
func (s *Storage) renewRefreshToken(currentRefreshToken, newRefreshToken, newAccessToken string) error {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
refreshToken, ok := s.refreshTokens[currentRefreshToken] refreshToken, ok := s.refreshTokens[currentRefreshToken]
if !ok { if !ok {
return "", "", fmt.Errorf("invalid refresh token") return fmt.Errorf("invalid refresh token")
} }
// deletes the refresh token and all access tokens which were issued based on this refresh token // deletes the refresh token
delete(s.refreshTokens, currentRefreshToken) delete(s.refreshTokens, currentRefreshToken)
for _, token := range s.tokens {
if token.RefreshTokenID == currentRefreshToken { // delete the access token which was issued based on this refresh token
delete(s.tokens, token.ID) delete(s.tokens, refreshToken.AccessToken)
break
} if refreshToken.Expiration.Before(time.Now()) {
return fmt.Errorf("expired refresh token")
} }
// creates a new refresh token based on the current one // creates a new refresh token based on the current one
token := uuid.NewString() refreshToken.Token = newRefreshToken
refreshToken.Token = token refreshToken.ID = newRefreshToken
refreshToken.ID = token refreshToken.Expiration = time.Now().Add(5 * time.Hour)
s.refreshTokens[token] = refreshToken refreshToken.AccessToken = newAccessToken
return token, refreshToken.ID, nil s.refreshTokens[newRefreshToken] = refreshToken
return nil
} }
// accessToken will store an access_token in-memory based on the provided information // accessToken will store an access_token in-memory based on the provided information

View file

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

20
go.mod
View file

@ -3,24 +3,24 @@ module github.com/zitadel/oidc/v3
go 1.21 go 1.21
require ( require (
github.com/bmatcuk/doublestar/v4 v4.7.1 github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/chi/v5 v5.2.1
github.com/go-jose/go-jose/v4 v4.0.4 github.com/go-jose/go-jose/v4 v4.0.5
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/google/go-github/v31 v31.0.0 github.com/google/go-github/v31 v31.0.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/securecookie v1.1.2 github.com/gorilla/securecookie v1.1.2
github.com/jeremija/gosubmit v0.2.7 github.com/jeremija/gosubmit v0.2.8
github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/gu v0.3.1
github.com/muhlemmer/httpforwarded v0.1.0 github.com/muhlemmer/httpforwarded v0.1.0
github.com/rs/cors v1.11.1 github.com/rs/cors v1.11.1
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.10.0
github.com/zitadel/logging v0.6.1 github.com/zitadel/logging v0.6.1
github.com/zitadel/schema v1.3.0 github.com/zitadel/schema v1.3.0
go.opentelemetry.io/otel v1.29.0 go.opentelemetry.io/otel v1.29.0
golang.org/x/oauth2 v0.23.0 golang.org/x/oauth2 v0.26.0
golang.org/x/text v0.19.0 golang.org/x/text v0.22.0
) )
require ( require (
@ -31,8 +31,8 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/crypto v0.25.0 // indirect golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.26.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

40
go.sum
View file

@ -1,12 +1,12 @@
github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -29,8 +29,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA=
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
@ -47,8 +47,8 @@ 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.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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y= github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y=
github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
@ -62,19 +62,19 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
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.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -83,13 +83,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.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/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
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=

View file

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

View file

@ -133,6 +133,7 @@ type Error struct {
ErrorType errorType `json:"error" schema:"error"` ErrorType errorType `json:"error" schema:"error"`
Description string `json:"error_description,omitempty" schema:"error_description,omitempty"` Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
State string `json:"state,omitempty" schema:"state,omitempty"` State string `json:"state,omitempty" schema:"state,omitempty"`
SessionState string `json:"session_state,omitempty" schema:"session_state,omitempty"`
redirectDisabled bool `schema:"-"` redirectDisabled bool `schema:"-"`
returnParent bool `schema:"-"` returnParent bool `schema:"-"`
} }
@ -142,11 +143,13 @@ func (e *Error) MarshalJSON() ([]byte, error) {
Error errorType `json:"error"` Error errorType `json:"error"`
ErrorDescription string `json:"error_description,omitempty"` ErrorDescription string `json:"error_description,omitempty"`
State string `json:"state,omitempty"` State string `json:"state,omitempty"`
SessionState string `json:"session_state,omitempty"`
Parent string `json:"parent,omitempty"` Parent string `json:"parent,omitempty"`
}{ }{
Error: e.ErrorType, Error: e.ErrorType,
ErrorDescription: e.Description, ErrorDescription: e.Description,
State: e.State, State: e.State,
SessionState: e.SessionState,
} }
if e.returnParent { if e.returnParent {
m.Parent = e.Parent.Error() m.Parent = e.Parent.Error()
@ -176,7 +179,8 @@ func (e *Error) Is(target error) bool {
} }
return e.ErrorType == t.ErrorType && return e.ErrorType == t.ErrorType &&
(e.Description == t.Description || t.Description == "") && (e.Description == t.Description || t.Description == "") &&
(e.State == t.State || t.State == "") (e.State == t.State || t.State == "") &&
(e.SessionState == t.SessionState || t.SessionState == "")
} }
func (e *Error) WithParent(err error) *Error { func (e *Error) WithParent(err error) *Error {
@ -242,6 +246,9 @@ func (e *Error) LogValue() slog.Value {
if e.State != "" { if e.State != "" {
attrs = append(attrs, slog.String("state", e.State)) attrs = append(attrs, slog.String("state", e.State))
} }
if e.SessionState != "" {
attrs = append(attrs, slog.String("session_state", e.SessionState))
}
if e.redirectDisabled { if e.redirectDisabled {
attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled)) attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled))
} }

View file

@ -236,6 +236,7 @@ type AccessTokenResponse struct {
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"` ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"` IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
State string `json:"state,omitempty" schema:"state,omitempty"` State string `json:"state,omitempty" schema:"state,omitempty"`
Scope SpaceDelimitedArray `json:"scope,omitempty" schema:"scope,omitempty"`
} }
type JWTProfileAssertionClaims struct { type JWTProfileAssertionClaims struct {

View file

@ -7,12 +7,11 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"slices"
"strings" "strings"
"time" "time"
jose "github.com/go-jose/go-jose/v4" jose "github.com/go-jose/go-jose/v4"
str "github.com/zitadel/oidc/v3/pkg/strings"
) )
type Claims interface { type Claims interface {
@ -84,7 +83,7 @@ type ACRVerifier func(string) error
// if none of the provided values matches the acr claim // if none of the provided values matches the acr claim
func DefaultACRVerifier(possibleValues []string) ACRVerifier { func DefaultACRVerifier(possibleValues []string) ACRVerifier {
return func(acr string) error { return func(acr string) error {
if !str.Contains(possibleValues, acr) { if !slices.Contains(possibleValues, acr) {
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr) return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
} }
return nil return nil
@ -123,7 +122,7 @@ func CheckIssuer(claims Claims, issuer string) error {
} }
func CheckAudience(claims Claims, clientID string) error { func CheckAudience(claims Claims, clientID string) error {
if !str.Contains(claims.GetAudience(), clientID) { if !slices.Contains(claims.GetAudience(), clientID) {
return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID) return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID)
} }

View file

@ -18,7 +18,6 @@ import (
"github.com/bmatcuk/doublestar/v4" "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"
) )
type AuthRequest interface { type AuthRequest interface {
@ -39,6 +38,13 @@ type AuthRequest interface {
Done() bool Done() bool
} }
// AuthRequestSessionState should be implemented if [OpenID Connect Session Management](https://openid.net/specs/openid-connect-session-1_0.html) is supported
type AuthRequestSessionState interface {
// GetSessionState returns session_state.
// session_state is related to OpenID Connect Session Management.
GetSessionState() string
}
type Authorizer interface { type Authorizer interface {
Storage() Storage Storage() Storage
Decoder() httphelper.Decoder Decoder() httphelper.Decoder
@ -104,8 +110,8 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
} }
return ValidateAuthRequestClient(ctx, authReq, client, verifier) return ValidateAuthRequestClient(ctx, authReq, client, verifier)
} }
if validater, ok := authorizer.(AuthorizeValidator); ok { if validator, ok := authorizer.(AuthorizeValidator); ok {
validation = validater.ValidateAuthRequest validation = validator.ValidateAuthRequest
} }
userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx)) userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx))
if err != nil { if err != nil {
@ -156,7 +162,7 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage
if requestObject.Issuer != requestObject.ClientID { if requestObject.Issuer != requestObject.ClientID {
return oidc.ErrInvalidRequest().WithDescription("missing or wrong issuer in request") return oidc.ErrInvalidRequest().WithDescription("missing or wrong issuer in request")
} }
if !str.Contains(requestObject.Audience, issuer) { if !slices.Contains(requestObject.Audience, issuer) {
return oidc.ErrInvalidRequest().WithDescription("issuer missing in audience") return oidc.ErrInvalidRequest().WithDescription("issuer missing in audience")
} }
keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer} keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer}
@ -170,7 +176,7 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage
// CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request // CopyRequestObjectToAuthRequest overwrites present values from the Request Object into the auth request
// and clears the `RequestParam` of the auth request // and clears the `RequestParam` of the auth request
func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) { func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) {
if str.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 { if slices.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 {
authReq.Scopes = requestObject.Scopes authReq.Scopes = requestObject.Scopes
} }
if requestObject.RedirectURI != "" { if requestObject.RedirectURI != "" {
@ -288,7 +294,7 @@ func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) {
// checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores // checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores
// other factors. // other factors.
func checkURIAgainstRedirects(client Client, uri string) error { func checkURIAgainstRedirects(client Client, uri string) error {
if str.Contains(client.RedirectURIs(), uri) { if slices.Contains(client.RedirectURIs(), uri) {
return nil return nil
} }
if globClient, ok := client.(HasRedirectGlobs); ok { if globClient, ok := client.(HasRedirectGlobs); ok {
@ -313,12 +319,12 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res
return oidc.ErrInvalidRequestRedirectURI().WithDescription("The redirect_uri is missing in the request. " + return oidc.ErrInvalidRequestRedirectURI().WithDescription("The redirect_uri is missing in the request. " +
"Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.") "Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.")
} }
if strings.HasPrefix(uri, "https://") {
return checkURIAgainstRedirects(client, uri)
}
if client.ApplicationType() == ApplicationTypeNative { if client.ApplicationType() == ApplicationTypeNative {
return validateAuthReqRedirectURINative(client, uri) return validateAuthReqRedirectURINative(client, uri)
} }
if strings.HasPrefix(uri, "https://") {
return checkURIAgainstRedirects(client, uri)
}
if err := checkURIAgainstRedirects(client, uri); err != nil { if err := checkURIAgainstRedirects(client, uri); err != nil {
return err return err
} }
@ -339,12 +345,15 @@ func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.Res
// ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type // ValidateAuthReqRedirectURINative validates the passed redirect_uri and response_type to the registered uris and client type
func validateAuthReqRedirectURINative(client Client, uri string) error { func validateAuthReqRedirectURINative(client Client, uri string) error {
parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri) parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri)
isCustomSchema := !strings.HasPrefix(uri, "http://") isCustomSchema := !(strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://"))
if err := checkURIAgainstRedirects(client, uri); err == nil { if err := checkURIAgainstRedirects(client, uri); err == nil {
if client.DevMode() { if client.DevMode() {
return nil return nil
} }
// The RedirectURIs are only valid for native clients when localhost or non-"http://" if !isLoopback && strings.HasPrefix(uri, "https://") {
return nil
}
// The RedirectURIs are only valid for native clients when localhost or non-"http://" and "https://"
if isLoopback || isCustomSchema { if isLoopback || isCustomSchema {
return nil return nil
} }
@ -374,11 +383,11 @@ func HTTPLoopbackOrLocalhost(rawURL string) (*url.URL, bool) {
if err != nil { if err != nil {
return nil, false return nil, false
} }
if parsedURL.Scheme != "http" { if parsedURL.Scheme == "http" || parsedURL.Scheme == "https" {
return nil, false
}
hostName := parsedURL.Hostname() hostName := parsedURL.Hostname()
return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback() return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback()
}
return nil, false
} }
// ValidateAuthReqResponseType validates the passed response_type to the registered response types // ValidateAuthReqResponseType validates the passed response_type to the registered response types
@ -479,12 +488,19 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques
AuthRequestError(w, r, authReq, err, authorizer) AuthRequestError(w, r, authReq, err, authorizer)
return return
} }
var sessionState string
authRequestSessionState, ok := authReq.(AuthRequestSessionState)
if ok {
sessionState = authRequestSessionState.GetSessionState()
}
codeResponse := struct { codeResponse := struct {
Code string `schema:"code"` Code string `schema:"code"`
State string `schema:"state,omitempty"` State string `schema:"state,omitempty"`
SessionState string `schema:"session_state,omitempty"`
}{ }{
Code: code, Code: code,
State: authReq.GetState(), State: authReq.GetState(),
SessionState: sessionState,
} }
if authReq.GetResponseMode() == oidc.ResponseModeFormPost { if authReq.GetResponseMode() == oidc.ResponseModeFormPost {

View file

@ -433,6 +433,24 @@ func TestValidateAuthReqRedirectURI(t *testing.T) {
}, },
false, false,
}, },
{
"code flow registered https loopback v4 native ok",
args{
"https://127.0.0.1:4200/callback",
mock.NewClientWithConfig(t, []string{"https://127.0.0.1/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{
"code flow registered https loopback v6 native ok",
args{
"https://[::1]:4200/callback",
mock.NewClientWithConfig(t, []string{"https://[::1]/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode,
},
false,
},
{ {
"code flow unregistered http native fails", "code flow unregistered http native fails",
args{ args{
@ -1072,6 +1090,34 @@ func TestAuthResponseCode(t *testing.T) {
wantBody: "", wantBody: "",
}, },
}, },
{
name: "success with state and session_state",
args: args{
authReq: &storage.AuthRequestWithSessionState{
AuthRequest: &storage.AuthRequest{
ID: "id1",
TransferState: "state1",
},
SessionState: "session_state1",
},
authorizer: func(t *testing.T) op.Authorizer {
ctrl := gomock.NewController(t)
storage := mock.NewMockStorage(ctrl)
storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1")
authorizer := mock.NewMockAuthorizer(ctrl)
authorizer.EXPECT().Storage().Return(storage)
authorizer.EXPECT().Crypto().Return(&mockCrypto{})
authorizer.EXPECT().Encoder().Return(schema.NewEncoder())
return authorizer
},
},
res: res{
wantCode: http.StatusFound,
wantLocationHeader: "/auth/callback/?code=id1&session_state=session_state1&state=state1",
wantBody: "",
},
},
{ {
name: "success without state", // reproduce issue #415 name: "success without state", // reproduce issue #415
args: args{ args: args{

View file

@ -30,6 +30,7 @@ type Configuration interface {
EndSessionEndpoint() *Endpoint EndSessionEndpoint() *Endpoint
KeysEndpoint() *Endpoint KeysEndpoint() *Endpoint
DeviceAuthorizationEndpoint() *Endpoint DeviceAuthorizationEndpoint() *Endpoint
CheckSessionIframe() *Endpoint
AuthMethodPostSupported() bool AuthMethodPostSupported() bool
CodeMethodS256Supported() bool CodeMethodS256Supported() bool

View file

@ -9,12 +9,12 @@ import (
"math/big" "math/big"
"net/http" "net/http"
"net/url" "net/url"
"slices"
"strings" "strings"
"time" "time"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "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 {
@ -276,7 +276,7 @@ func (r *DeviceAuthorizationState) GetAMR() []string {
} }
func (r *DeviceAuthorizationState) GetAudience() []string { func (r *DeviceAuthorizationState) GetAudience() []string {
if !strs.Contains(r.Audience, r.ClientID) { if !slices.Contains(r.Audience, r.ClientID) {
r.Audience = append(r.Audience, r.ClientID) r.Audience = append(r.Audience, r.ClientID)
} }
return r.Audience return r.Audience
@ -344,10 +344,11 @@ func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, c
RefreshToken: refreshToken, RefreshToken: refreshToken,
TokenType: oidc.BearerToken, TokenType: oidc.BearerToken,
ExpiresIn: uint64(validity.Seconds()), ExpiresIn: uint64(validity.Seconds()),
Scope: tokenRequest.GetScopes(),
} }
// TODO(v4): remove type assertion // TODO(v4): remove type assertion
if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && strs.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) { if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && slices.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) {
response.IDToken, err = CreateIDToken(ctx, IssuerFromContext(ctx), idTokenRequest, client.IDTokenLifetime(), accessToken, "", creator.Storage(), client) response.IDToken, err = CreateIDToken(ctx, IssuerFromContext(ctx), idTokenRequest, client.IDTokenLifetime(), accessToken, "", creator.Storage(), client)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -45,6 +45,7 @@ func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage Di
EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer), EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer),
JwksURI: config.KeysEndpoint().Absolute(issuer), JwksURI: config.KeysEndpoint().Absolute(issuer),
DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer), DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer),
CheckSessionIframe: config.CheckSessionIframe().Absolute(issuer),
ScopesSupported: Scopes(config), ScopesSupported: Scopes(config),
ResponseTypesSupported: ResponseTypes(config), ResponseTypesSupported: ResponseTypes(config),
GrantTypesSupported: GrantTypes(config), GrantTypesSupported: GrantTypes(config),
@ -100,7 +101,11 @@ func createDiscoveryConfigV2(ctx context.Context, config Configuration, storage
} }
func Scopes(c Configuration) []string { func Scopes(c Configuration) []string {
return DefaultSupportedScopes // TODO: config provider, ok := c.(*Provider)
if ok && provider.config.SupportedScopes != nil {
return provider.config.SupportedScopes
}
return DefaultSupportedScopes
} }
func ResponseTypes(c Configuration) []string { func ResponseTypes(c Configuration) []string {
@ -135,7 +140,7 @@ func GrantTypes(c Configuration) []oidc.GrantType {
} }
func SubjectTypes(c Configuration) []string { func SubjectTypes(c Configuration) []string {
return []string{"public"} //TODO: config return []string{"public"} // TODO: config
} }
func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string { func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string {

View file

@ -81,6 +81,11 @@ func Test_scopes(t *testing.T) {
args{}, args{},
op.DefaultSupportedScopes, op.DefaultSupportedScopes,
}, },
{
"custom scopes",
args{newTestProvider(&op.Config{SupportedScopes: []string{"test1", "test2"}})},
[]string{"test1", "test2"},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View file

@ -46,6 +46,12 @@ func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthReq
return return
} }
e.State = authReq.GetState() e.State = authReq.GetState()
var sessionState string
authRequestSessionState, ok := authReq.(AuthRequestSessionState)
if ok {
sessionState = authRequestSessionState.GetSessionState()
}
e.SessionState = sessionState
var responseMode oidc.ResponseMode var responseMode oidc.ResponseMode
if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok { if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok {
responseMode = rm.GetResponseMode() responseMode = rm.GetResponseMode()
@ -92,6 +98,12 @@ func TryErrorRedirect(ctx context.Context, authReq ErrAuthRequest, parent error,
} }
e.State = authReq.GetState() e.State = authReq.GetState()
var sessionState string
authRequestSessionState, ok := authReq.(AuthRequestSessionState)
if ok {
sessionState = authRequestSessionState.GetSessionState()
}
e.SessionState = sessionState
var responseMode oidc.ResponseMode var responseMode oidc.ResponseMode
if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok { if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok {
responseMode = rm.GetResponseMode() responseMode = rm.GetResponseMode()

View file

@ -106,6 +106,20 @@ func (mr *MockConfigurationMockRecorder) BackChannelLogoutSupported() *gomock.Ca
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackChannelLogoutSupported", reflect.TypeOf((*MockConfiguration)(nil).BackChannelLogoutSupported)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackChannelLogoutSupported", reflect.TypeOf((*MockConfiguration)(nil).BackChannelLogoutSupported))
} }
// CheckSessionIframe mocks base method.
func (m *MockConfiguration) CheckSessionIframe() *op.Endpoint {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CheckSessionIframe")
ret0, _ := ret[0].(*op.Endpoint)
return ret0
}
// CheckSessionIframe indicates an expected call of CheckSessionIframe.
func (mr *MockConfigurationMockRecorder) CheckSessionIframe() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckSessionIframe", reflect.TypeOf((*MockConfiguration)(nil).CheckSessionIframe))
}
// CodeMethodS256Supported mocks base method. // CodeMethodS256Supported mocks base method.
func (m *MockConfiguration) CodeMethodS256Supported() bool { func (m *MockConfiguration) CodeMethodS256Supported() bool {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View file

@ -167,6 +167,7 @@ type Config struct {
RequestObjectSupported bool RequestObjectSupported bool
SupportedUILocales []language.Tag SupportedUILocales []language.Tag
SupportedClaims []string SupportedClaims []string
SupportedScopes []string
DeviceAuthorization DeviceAuthorizationConfig DeviceAuthorization DeviceAuthorizationConfig
BackChannelLogoutSupported bool BackChannelLogoutSupported bool
BackChannelLogoutSessionSupported bool BackChannelLogoutSessionSupported bool
@ -338,6 +339,10 @@ func (o *Provider) DeviceAuthorizationEndpoint() *Endpoint {
return o.endpoints.DeviceAuthorization return o.endpoints.DeviceAuthorization
} }
func (o *Provider) CheckSessionIframe() *Endpoint {
return o.endpoints.CheckSessionIframe
}
func (o *Provider) KeysEndpoint() *Endpoint { func (o *Provider) KeysEndpoint() *Endpoint {
return o.endpoints.JwksURI return o.endpoints.JwksURI
} }

View file

@ -232,7 +232,7 @@ func TestRoutes(t *testing.T) {
"scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(),
}, },
wantCode: http.StatusOK, wantCode: http.StatusOK,
contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`}, contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299,"scope":"openid offline_access"}`},
}, },
{ {
// This call will fail. A successful test is already // This call will fail. A successful test is already

View file

@ -145,7 +145,7 @@ func TestServerRoutes(t *testing.T) {
"assertion": jwtProfileToken, "assertion": jwtProfileToken,
}, },
wantCode: http.StatusOK, wantCode: http.StatusOK,
contains: []string{`{"access_token":`, `"token_type":"Bearer","expires_in":299}`}, contains: []string{`{"access_token":`, `"token_type":"Bearer","expires_in":299,"scope":"openid"}`},
}, },
{ {
name: "Token exchange", name: "Token exchange",
@ -174,7 +174,7 @@ func TestServerRoutes(t *testing.T) {
"scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(), "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(),
}, },
wantCode: http.StatusOK, wantCode: http.StatusOK,
contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`}, contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299,"scope":"openid offline_access"}`},
}, },
{ {
// This call will fail. A successful test is already // This call will fail. A successful test is already

View file

@ -2,11 +2,11 @@ package op
import ( import (
"context" "context"
"slices"
"time" "time"
"github.com/zitadel/oidc/v3/pkg/crypto" "github.com/zitadel/oidc/v3/pkg/crypto"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/strings"
) )
type TokenCreator interface { type TokenCreator interface {
@ -65,6 +65,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli
TokenType: oidc.BearerToken, TokenType: oidc.BearerToken,
ExpiresIn: exp, ExpiresIn: exp,
State: state, State: state,
Scope: request.GetScopes(),
}, nil }, nil
} }
@ -82,13 +83,13 @@ func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storag
func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool { func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool {
switch req := tokenRequest.(type) { switch req := tokenRequest.(type) {
case AuthRequest: case AuthRequest:
return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken) return slices.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken)
case TokenExchangeRequest: case TokenExchangeRequest:
return req.GetRequestedTokenType() == oidc.RefreshTokenType return req.GetRequestedTokenType() == oidc.RefreshTokenType
case RefreshTokenRequest: case RefreshTokenRequest:
return true return true
case *DeviceAuthorizationState: case *DeviceAuthorizationState:
return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && ValidateGrantType(client, oidc.GrantTypeRefreshToken) return slices.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && ValidateGrantType(client, oidc.GrantTypeRefreshToken)
default: default:
return false return false
} }

View file

@ -120,5 +120,6 @@ func CreateClientCredentialsTokenResponse(ctx context.Context, tokenRequest Toke
AccessToken: accessToken, AccessToken: accessToken,
TokenType: oidc.BearerToken, TokenType: oidc.BearerToken,
ExpiresIn: uint64(validity.Seconds()), ExpiresIn: uint64(validity.Seconds()),
Scope: tokenRequest.GetScopes(),
}, nil }, nil
} }

View file

@ -89,6 +89,7 @@ func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, crea
AccessToken: accessToken, AccessToken: accessToken,
TokenType: oidc.BearerToken, TokenType: oidc.BearerToken,
ExpiresIn: uint64(validity.Seconds()), ExpiresIn: uint64(validity.Seconds()),
Scope: tokenRequest.GetScopes(),
}, nil }, nil
} }

View file

@ -4,11 +4,11 @@ import (
"context" "context"
"errors" "errors"
"net/http" "net/http"
"slices"
"time" "time"
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"
"github.com/zitadel/oidc/v3/pkg/strings"
) )
type RefreshTokenRequest interface { type RefreshTokenRequest interface {
@ -85,7 +85,7 @@ func ValidateRefreshTokenScopes(requestedScopes []string, authRequest RefreshTok
return nil return nil
} }
for _, scope := range requestedScopes { for _, scope := range requestedScopes {
if !strings.Contains(authRequest.GetScopes(), scope) { if !slices.Contains(authRequest.GetScopes(), scope) {
return oidc.ErrInvalidScope() return oidc.ErrInvalidScope()
} }
} }

View file

@ -1,10 +1,9 @@
package strings package strings
import "slices"
// Deprecated: Use standard library [slices.Contains] instead.
func Contains(list []string, needle string) bool { func Contains(list []string, needle string) bool {
for _, item := range list { // TODO(v4): remove package.
if item == needle { return slices.Contains(list, needle)
return true
}
}
return false
} }