Merge branch 'main' into fix-empty-locale
This commit is contained in:
commit
7dcd1ec03c
28 changed files with 284 additions and 143 deletions
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: ['1.21', '1.22']
|
||||
go: ['1.21', '1.22', '1.23']
|
||||
name: Go ${{ matrix.go }} test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -27,7 +27,7 @@ jobs:
|
|||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- 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:
|
||||
file: ./profile.cov
|
||||
name: codecov-go
|
||||
|
|
72
README.md
72
README.md
|
@ -21,6 +21,7 @@ Whenever possible we tried to reuse / extend existing packages like `OAuth2 for
|
|||
## Basic Overview
|
||||
|
||||
The most important packages of the library:
|
||||
|
||||
<pre>
|
||||
/pkg
|
||||
/client clients using the OP for retrieving, exchanging and verifying tokens
|
||||
|
@ -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
|
||||
</pre>
|
||||
|
||||
|
||||
### Semver
|
||||
|
||||
This package uses [semver](https://semver.org/) for [releases](https://github.com/zitadel/oidc/releases). Major releases ship breaking changes. Starting with the `v2` to `v3` increment we provide an [upgrade guide](UPGRADING.md) to ease migration to a newer version.
|
||||
|
@ -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
|
||||
|
||||
for the dynamic issuer, just start it with:
|
||||
|
||||
```bash
|
||||
go run github.com/zitadel/oidc/v3/example/server/dynamic
|
||||
```
|
||||
|
||||
the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with:
|
||||
|
||||
```bash
|
||||
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app
|
||||
```
|
||||
|
@ -75,35 +78,36 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid
|
|||
Example server allows extra configuration using environment variables and could be used for end to
|
||||
end testing of your services.
|
||||
|
||||
| Name | Format | Description |
|
||||
|---------------|--------------------------------------|---------------------------------------|
|
||||
| PORT | Number between 1 and 65535 | OIDC listen port |
|
||||
| REDIRECT_URI | Comma-separated URIs | List of allowed redirect URIs |
|
||||
| USERS_FILE | Path to json in local filesystem | Users with their data and credentials |
|
||||
| Name | Format | Description |
|
||||
| ------------ | -------------------------------- | ------------------------------------- |
|
||||
| PORT | Number between 1 and 65535 | OIDC listen port |
|
||||
| REDIRECT_URI | Comma-separated URIs | List of allowed redirect URIs |
|
||||
| USERS_FILE | Path to json in local filesystem | Users with their data and credentials |
|
||||
|
||||
Here is json equivalent for one of the default users
|
||||
|
||||
```json
|
||||
{
|
||||
"id2": {
|
||||
"ID": "id2",
|
||||
"Username": "test-user2",
|
||||
"Password": "verysecure",
|
||||
"FirstName": "Test",
|
||||
"LastName": "User2",
|
||||
"Email": "test-user2@zitadel.ch",
|
||||
"EmailVerified": true,
|
||||
"Phone": "",
|
||||
"PhoneVerified": false,
|
||||
"PreferredLanguage": "DE",
|
||||
"IsAdmin": false
|
||||
}
|
||||
"id2": {
|
||||
"ID": "id2",
|
||||
"Username": "test-user2",
|
||||
"Password": "verysecure",
|
||||
"FirstName": "Test",
|
||||
"LastName": "User2",
|
||||
"Email": "test-user2@zitadel.ch",
|
||||
"EmailVerified": true,
|
||||
"Phone": "",
|
||||
"PhoneVerified": false,
|
||||
"PreferredLanguage": "DE",
|
||||
"IsAdmin": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
| | Relying party | OpenID Provider | Specification |
|
||||
|----------------------| ------------- | --------------- |----------------------------------------------|
|
||||
| -------------------- | ------------- | --------------- | -------------------------------------------- |
|
||||
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] |
|
||||
| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] |
|
||||
| Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] |
|
||||
|
@ -117,18 +121,18 @@ Here is json equivalent for one of the default users
|
|||
| mTLS | not yet | not yet | [RFC 8705][11] |
|
||||
| Back-Channel Logout | not yet | yes | OpenID Connect [Back-Channel Logout][12] 1.0 |
|
||||
|
||||
[1]: <https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth> "3.1. Authentication using the Authorization Code Flow"
|
||||
[2]: <https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth> "3.2. Authentication using the Implicit Flow"
|
||||
[3]: <https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth> "3.3. Authentication using the Hybrid Flow"
|
||||
[4]: <https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication> "9. Client Authentication"
|
||||
[5]: <https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens> "12. Using Refresh Tokens"
|
||||
[6]: <https://openid.net/specs/openid-connect-discovery-1_0.html> "OpenID Connect Discovery 1.0 incorporating errata set 1"
|
||||
[7]: <https://www.rfc-editor.org/rfc/rfc7523.html> "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
|
||||
[8]: <https://www.rfc-editor.org/rfc/rfc7636.html> "Proof Key for Code Exchange by OAuth Public Clients"
|
||||
[9]: <https://www.rfc-editor.org/rfc/rfc8693.html> "OAuth 2.0 Token Exchange"
|
||||
[10]: <https://www.rfc-editor.org/rfc/rfc8628.html> "OAuth 2.0 Device Authorization Grant"
|
||||
[11]: <https://www.rfc-editor.org/rfc/rfc8705.html> "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
|
||||
[12]: <https://openid.net/specs/openid-connect-backchannel-1_0.html> "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1"
|
||||
[1]: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth "3.1. Authentication using the Authorization Code Flow"
|
||||
[2]: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth "3.2. Authentication using the Implicit Flow"
|
||||
[3]: https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth "3.3. Authentication using the Hybrid Flow"
|
||||
[4]: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication "9. Client Authentication"
|
||||
[5]: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens "12. Using Refresh Tokens"
|
||||
[6]: https://openid.net/specs/openid-connect-discovery-1_0.html "OpenID Connect Discovery 1.0 incorporating errata set 1"
|
||||
[7]: https://www.rfc-editor.org/rfc/rfc7523.html "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
|
||||
[8]: https://www.rfc-editor.org/rfc/rfc7636.html "Proof Key for Code Exchange by OAuth Public Clients"
|
||||
[9]: https://www.rfc-editor.org/rfc/rfc8693.html "OAuth 2.0 Token Exchange"
|
||||
[10]: https://www.rfc-editor.org/rfc/rfc8628.html "OAuth 2.0 Device Authorization Grant"
|
||||
[11]: https://www.rfc-editor.org/rfc/rfc8705.html "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
|
||||
[12]: https://openid.net/specs/openid-connect-backchannel-1_0.html "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1"
|
||||
|
||||
## Contributors
|
||||
|
||||
|
@ -153,8 +157,9 @@ Versions that also build are marked with :warning:.
|
|||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| <1.21 | :x: |
|
||||
| 1.21 | :white_check_mark: |
|
||||
| 1.21 | :warning: |
|
||||
| 1.22 | :white_check_mark: |
|
||||
| 1.23 | :white_check_mark: |
|
||||
|
||||
## 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
|
||||
language governing permissions and limitations under the License.
|
||||
|
||||
|
||||
[^1]: https://github.com/zitadel/oidc/issues/135#issuecomment-950563892
|
||||
|
|
|
@ -32,8 +32,8 @@ func main() {
|
|||
|
||||
storage.RegisterClients(
|
||||
storage.NativeClient("native", cfg.RedirectURI...),
|
||||
storage.WebClient("web", "secret"),
|
||||
storage.WebClient("api", "secret"),
|
||||
storage.WebClient("web", "secret", cfg.RedirectURI...),
|
||||
storage.WebClient("api", "secret", cfg.RedirectURI...),
|
||||
)
|
||||
|
||||
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
||||
|
|
|
@ -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 {
|
||||
Challenge string
|
||||
Method string
|
||||
|
|
|
@ -151,6 +151,9 @@ func (s *Storage) CheckUsernamePassword(username, password, id string) error {
|
|||
// in this example we'll simply check the username / password and set a boolean to true
|
||||
// therefore we will also just check this boolean if the request / login has been finished
|
||||
request.done = true
|
||||
|
||||
request.authTime = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("username or password wrong")
|
||||
|
@ -295,15 +298,19 @@ func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.T
|
|||
|
||||
// if we get here, the currentRefreshToken was not empty, so the call is a refresh token request
|
||||
// we therefore will have to check the currentRefreshToken and renew the refresh token
|
||||
refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken)
|
||||
|
||||
newRefreshToken = uuid.NewString()
|
||||
|
||||
accessToken, err := s.accessToken(applicationID, newRefreshToken, request.GetSubject(), request.GetAudience(), request.GetScopes())
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, err
|
||||
}
|
||||
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
|
||||
if err != nil {
|
||||
|
||||
if err := s.renewRefreshToken(currentRefreshToken, newRefreshToken, accessToken.ID); err != nil {
|
||||
return "", "", time.Time{}, err
|
||||
}
|
||||
return accessToken.ID, refreshToken, accessToken.Expiration, nil
|
||||
|
||||
return accessToken.ID, newRefreshToken, accessToken.Expiration, nil
|
||||
}
|
||||
|
||||
func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
|
||||
|
@ -385,14 +392,9 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID
|
|||
if refreshToken.ApplicationID != clientID {
|
||||
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
|
||||
}
|
||||
// if it is a refresh token, you will have to remove the access token as well
|
||||
delete(s.refreshTokens, refreshToken.ID)
|
||||
for _, accessToken := range s.tokens {
|
||||
if accessToken.RefreshTokenID == refreshToken.ID {
|
||||
delete(s.tokens, accessToken.ID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// if it is a refresh token, you will have to remove the access token as well
|
||||
delete(s.tokens, refreshToken.AccessToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -488,6 +490,9 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserI
|
|||
// return err
|
||||
// }
|
||||
//}
|
||||
if token.Expiration.Before(time.Now()) {
|
||||
return fmt.Errorf("token is expired")
|
||||
}
|
||||
return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes)
|
||||
}
|
||||
|
||||
|
@ -594,33 +599,41 @@ func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime
|
|||
Audience: accessToken.Audience,
|
||||
Expiration: time.Now().Add(5 * time.Hour),
|
||||
Scopes: accessToken.Scopes,
|
||||
AccessToken: accessToken.ID,
|
||||
}
|
||||
s.refreshTokens[token.ID] = token
|
||||
return token.Token, nil
|
||||
}
|
||||
|
||||
// renewRefreshToken checks the provided refresh_token and creates a new one based on the current
|
||||
func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) {
|
||||
//
|
||||
// [Refresh Token Rotation] is implemented.
|
||||
//
|
||||
// [Refresh Token Rotation]: https://www.rfc-editor.org/rfc/rfc6819#section-5.2.2.3
|
||||
func (s *Storage) renewRefreshToken(currentRefreshToken, newRefreshToken, newAccessToken string) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
refreshToken, ok := s.refreshTokens[currentRefreshToken]
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("invalid refresh token")
|
||||
return fmt.Errorf("invalid refresh token")
|
||||
}
|
||||
// deletes the refresh token and all access tokens which were issued based on this refresh token
|
||||
// deletes the refresh token
|
||||
delete(s.refreshTokens, currentRefreshToken)
|
||||
for _, token := range s.tokens {
|
||||
if token.RefreshTokenID == currentRefreshToken {
|
||||
delete(s.tokens, token.ID)
|
||||
break
|
||||
}
|
||||
|
||||
// delete the access token which was issued based on this refresh token
|
||||
delete(s.tokens, refreshToken.AccessToken)
|
||||
|
||||
if refreshToken.Expiration.Before(time.Now()) {
|
||||
return fmt.Errorf("expired refresh token")
|
||||
}
|
||||
|
||||
// creates a new refresh token based on the current one
|
||||
token := uuid.NewString()
|
||||
refreshToken.Token = token
|
||||
refreshToken.ID = token
|
||||
s.refreshTokens[token] = refreshToken
|
||||
return token, refreshToken.ID, nil
|
||||
refreshToken.Token = newRefreshToken
|
||||
refreshToken.ID = newRefreshToken
|
||||
refreshToken.Expiration = time.Now().Add(5 * time.Hour)
|
||||
refreshToken.AccessToken = newAccessToken
|
||||
s.refreshTokens[newRefreshToken] = refreshToken
|
||||
return nil
|
||||
}
|
||||
|
||||
// accessToken will store an access_token in-memory based on the provided information
|
||||
|
|
|
@ -22,4 +22,5 @@ type RefreshToken struct {
|
|||
ApplicationID string
|
||||
Expiration time.Time
|
||||
Scopes []string
|
||||
AccessToken string // Token.ID
|
||||
}
|
||||
|
|
20
go.mod
20
go.mod
|
@ -3,24 +3,24 @@ module github.com/zitadel/oidc/v3
|
|||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/bmatcuk/doublestar/v4 v4.7.1
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/go-jose/go-jose/v4 v4.0.4
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-jose/go-jose/v4 v4.0.5
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-github/v31 v31.0.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/jeremija/gosubmit v0.2.7
|
||||
github.com/jeremija/gosubmit v0.2.8
|
||||
github.com/muhlemmer/gu v0.3.1
|
||||
github.com/muhlemmer/httpforwarded v0.1.0
|
||||
github.com/rs/cors v1.11.1
|
||||
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/schema v1.3.0
|
||||
go.opentelemetry.io/otel v1.29.0
|
||||
golang.org/x/oauth2 v0.23.0
|
||||
golang.org/x/text v0.19.0
|
||||
golang.org/x/oauth2 v0.26.0
|
||||
golang.org/x/text v0.22.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -31,8 +31,8 @@ require (
|
|||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
40
go.sum
40
go.sum
|
@ -1,12 +1,12 @@
|
|||
github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
|
||||
github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
|
@ -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/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
|
||||
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
|
||||
github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA=
|
||||
github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
|
@ -47,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/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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y=
|
||||
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=
|
||||
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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
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/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-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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
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.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
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/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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
|
|
|
@ -2,7 +2,7 @@ package client
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -24,7 +24,7 @@ type KeyFile struct {
|
|||
}
|
||||
|
||||
func ConfigFromKeyFile(path string) (*KeyFile, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -133,6 +133,7 @@ type Error struct {
|
|||
ErrorType errorType `json:"error" schema:"error"`
|
||||
Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
|
||||
State string `json:"state,omitempty" schema:"state,omitempty"`
|
||||
SessionState string `json:"session_state,omitempty" schema:"session_state,omitempty"`
|
||||
redirectDisabled bool `schema:"-"`
|
||||
returnParent bool `schema:"-"`
|
||||
}
|
||||
|
@ -142,11 +143,13 @@ func (e *Error) MarshalJSON() ([]byte, error) {
|
|||
Error errorType `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
SessionState string `json:"session_state,omitempty"`
|
||||
Parent string `json:"parent,omitempty"`
|
||||
}{
|
||||
Error: e.ErrorType,
|
||||
ErrorDescription: e.Description,
|
||||
State: e.State,
|
||||
SessionState: e.SessionState,
|
||||
}
|
||||
if e.returnParent {
|
||||
m.Parent = e.Parent.Error()
|
||||
|
@ -176,7 +179,8 @@ func (e *Error) Is(target error) bool {
|
|||
}
|
||||
return e.ErrorType == t.ErrorType &&
|
||||
(e.Description == t.Description || t.Description == "") &&
|
||||
(e.State == t.State || t.State == "")
|
||||
(e.State == t.State || t.State == "") &&
|
||||
(e.SessionState == t.SessionState || t.SessionState == "")
|
||||
}
|
||||
|
||||
func (e *Error) WithParent(err error) *Error {
|
||||
|
@ -242,6 +246,9 @@ func (e *Error) LogValue() slog.Value {
|
|||
if e.State != "" {
|
||||
attrs = append(attrs, slog.String("state", e.State))
|
||||
}
|
||||
if e.SessionState != "" {
|
||||
attrs = append(attrs, slog.String("session_state", e.SessionState))
|
||||
}
|
||||
if e.redirectDisabled {
|
||||
attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled))
|
||||
}
|
||||
|
|
|
@ -230,12 +230,13 @@ func (c *ActorClaims) UnmarshalJSON(data []byte) error {
|
|||
}
|
||||
|
||||
type AccessTokenResponse struct {
|
||||
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
|
||||
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
|
||||
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
|
||||
State string `json:"state,omitempty" schema:"state,omitempty"`
|
||||
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
|
||||
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
|
||||
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
|
||||
State string `json:"state,omitempty" schema:"state,omitempty"`
|
||||
Scope SpaceDelimitedArray `json:"scope,omitempty" schema:"scope,omitempty"`
|
||||
}
|
||||
|
||||
type JWTProfileAssertionClaims struct {
|
||||
|
|
|
@ -7,12 +7,11 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
|
||||
str "github.com/zitadel/oidc/v3/pkg/strings"
|
||||
)
|
||||
|
||||
type Claims interface {
|
||||
|
@ -84,7 +83,7 @@ type ACRVerifier func(string) error
|
|||
// if none of the provided values matches the acr claim
|
||||
func DefaultACRVerifier(possibleValues []string) ACRVerifier {
|
||||
return func(acr string) error {
|
||||
if !str.Contains(possibleValues, acr) {
|
||||
if !slices.Contains(possibleValues, acr) {
|
||||
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
|
||||
}
|
||||
return nil
|
||||
|
@ -123,7 +122,7 @@ func CheckIssuer(claims Claims, issuer string) error {
|
|||
}
|
||||
|
||||
func CheckAudience(claims Claims, clientID string) error {
|
||||
if !str.Contains(claims.GetAudience(), clientID) {
|
||||
if !slices.Contains(claims.GetAudience(), clientID) {
|
||||
return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import (
|
|||
"github.com/bmatcuk/doublestar/v4"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
str "github.com/zitadel/oidc/v3/pkg/strings"
|
||||
)
|
||||
|
||||
type AuthRequest interface {
|
||||
|
@ -39,6 +38,13 @@ type AuthRequest interface {
|
|||
Done() bool
|
||||
}
|
||||
|
||||
// AuthRequestSessionState should be implemented if [OpenID Connect Session Management](https://openid.net/specs/openid-connect-session-1_0.html) is supported
|
||||
type AuthRequestSessionState interface {
|
||||
// GetSessionState returns session_state.
|
||||
// session_state is related to OpenID Connect Session Management.
|
||||
GetSessionState() string
|
||||
}
|
||||
|
||||
type Authorizer interface {
|
||||
Storage() Storage
|
||||
Decoder() httphelper.Decoder
|
||||
|
@ -104,8 +110,8 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
|||
}
|
||||
return ValidateAuthRequestClient(ctx, authReq, client, verifier)
|
||||
}
|
||||
if validater, ok := authorizer.(AuthorizeValidator); ok {
|
||||
validation = validater.ValidateAuthRequest
|
||||
if validator, ok := authorizer.(AuthorizeValidator); ok {
|
||||
validation = validator.ValidateAuthRequest
|
||||
}
|
||||
userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx))
|
||||
if err != nil {
|
||||
|
@ -156,7 +162,7 @@ func ParseRequestObject(ctx context.Context, authReq *oidc.AuthRequest, storage
|
|||
if requestObject.Issuer != requestObject.ClientID {
|
||||
return oidc.ErrInvalidRequest().WithDescription("missing or wrong issuer in request")
|
||||
}
|
||||
if !str.Contains(requestObject.Audience, issuer) {
|
||||
if !slices.Contains(requestObject.Audience, issuer) {
|
||||
return oidc.ErrInvalidRequest().WithDescription("issuer missing in audience")
|
||||
}
|
||||
keySet := &jwtProfileKeySet{storage: storage, clientID: requestObject.Issuer}
|
||||
|
@ -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
|
||||
// and clears the `RequestParam` of the auth request
|
||||
func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oidc.RequestObject) {
|
||||
if str.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 {
|
||||
if slices.Contains(authReq.Scopes, oidc.ScopeOpenID) && len(requestObject.Scopes) > 0 {
|
||||
authReq.Scopes = requestObject.Scopes
|
||||
}
|
||||
if requestObject.RedirectURI != "" {
|
||||
|
@ -288,7 +294,7 @@ func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) {
|
|||
// checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores
|
||||
// other factors.
|
||||
func checkURIAgainstRedirects(client Client, uri string) error {
|
||||
if str.Contains(client.RedirectURIs(), uri) {
|
||||
if slices.Contains(client.RedirectURIs(), uri) {
|
||||
return nil
|
||||
}
|
||||
if globClient, ok := client.(HasRedirectGlobs); ok {
|
||||
|
@ -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. " +
|
||||
"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 {
|
||||
return validateAuthReqRedirectURINative(client, uri)
|
||||
}
|
||||
if strings.HasPrefix(uri, "https://") {
|
||||
return checkURIAgainstRedirects(client, uri)
|
||||
}
|
||||
if err := checkURIAgainstRedirects(client, uri); err != nil {
|
||||
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
|
||||
func validateAuthReqRedirectURINative(client Client, uri string) error {
|
||||
parsedURL, isLoopback := HTTPLoopbackOrLocalhost(uri)
|
||||
isCustomSchema := !strings.HasPrefix(uri, "http://")
|
||||
isCustomSchema := !(strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://"))
|
||||
if err := checkURIAgainstRedirects(client, uri); err == nil {
|
||||
if client.DevMode() {
|
||||
return nil
|
||||
}
|
||||
// The RedirectURIs are only valid for native clients when localhost or non-"http://"
|
||||
if !isLoopback && strings.HasPrefix(uri, "https://") {
|
||||
return nil
|
||||
}
|
||||
// The RedirectURIs are only valid for native clients when localhost or non-"http://" and "https://"
|
||||
if isLoopback || isCustomSchema {
|
||||
return nil
|
||||
}
|
||||
|
@ -374,11 +383,11 @@ func HTTPLoopbackOrLocalhost(rawURL string) (*url.URL, bool) {
|
|||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
if parsedURL.Scheme != "http" {
|
||||
return nil, false
|
||||
if parsedURL.Scheme == "http" || parsedURL.Scheme == "https" {
|
||||
hostName := parsedURL.Hostname()
|
||||
return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback()
|
||||
}
|
||||
hostName := parsedURL.Hostname()
|
||||
return parsedURL, hostName == "localhost" || net.ParseIP(hostName).IsLoopback()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ValidateAuthReqResponseType validates the passed response_type to the registered response types
|
||||
|
@ -479,12 +488,19 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques
|
|||
AuthRequestError(w, r, authReq, err, authorizer)
|
||||
return
|
||||
}
|
||||
var sessionState string
|
||||
authRequestSessionState, ok := authReq.(AuthRequestSessionState)
|
||||
if ok {
|
||||
sessionState = authRequestSessionState.GetSessionState()
|
||||
}
|
||||
codeResponse := struct {
|
||||
Code string `schema:"code"`
|
||||
State string `schema:"state,omitempty"`
|
||||
Code string `schema:"code"`
|
||||
State string `schema:"state,omitempty"`
|
||||
SessionState string `schema:"session_state,omitempty"`
|
||||
}{
|
||||
Code: code,
|
||||
State: authReq.GetState(),
|
||||
Code: code,
|
||||
State: authReq.GetState(),
|
||||
SessionState: sessionState,
|
||||
}
|
||||
|
||||
if authReq.GetResponseMode() == oidc.ResponseModeFormPost {
|
||||
|
|
|
@ -433,6 +433,24 @@ func TestValidateAuthReqRedirectURI(t *testing.T) {
|
|||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"code flow registered https loopback v4 native ok",
|
||||
args{
|
||||
"https://127.0.0.1:4200/callback",
|
||||
mock.NewClientWithConfig(t, []string{"https://127.0.0.1/callback"}, op.ApplicationTypeNative, nil, false),
|
||||
oidc.ResponseTypeCode,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"code flow registered https loopback v6 native ok",
|
||||
args{
|
||||
"https://[::1]:4200/callback",
|
||||
mock.NewClientWithConfig(t, []string{"https://[::1]/callback"}, op.ApplicationTypeNative, nil, false),
|
||||
oidc.ResponseTypeCode,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"code flow unregistered http native fails",
|
||||
args{
|
||||
|
@ -1072,6 +1090,34 @@ func TestAuthResponseCode(t *testing.T) {
|
|||
wantBody: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success with state and session_state",
|
||||
args: args{
|
||||
authReq: &storage.AuthRequestWithSessionState{
|
||||
AuthRequest: &storage.AuthRequest{
|
||||
ID: "id1",
|
||||
TransferState: "state1",
|
||||
},
|
||||
SessionState: "session_state1",
|
||||
},
|
||||
authorizer: func(t *testing.T) op.Authorizer {
|
||||
ctrl := gomock.NewController(t)
|
||||
storage := mock.NewMockStorage(ctrl)
|
||||
storage.EXPECT().SaveAuthCode(gomock.Any(), "id1", "id1")
|
||||
|
||||
authorizer := mock.NewMockAuthorizer(ctrl)
|
||||
authorizer.EXPECT().Storage().Return(storage)
|
||||
authorizer.EXPECT().Crypto().Return(&mockCrypto{})
|
||||
authorizer.EXPECT().Encoder().Return(schema.NewEncoder())
|
||||
return authorizer
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
wantCode: http.StatusFound,
|
||||
wantLocationHeader: "/auth/callback/?code=id1&session_state=session_state1&state=state1",
|
||||
wantBody: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success without state", // reproduce issue #415
|
||||
args: args{
|
||||
|
|
|
@ -30,6 +30,7 @@ type Configuration interface {
|
|||
EndSessionEndpoint() *Endpoint
|
||||
KeysEndpoint() *Endpoint
|
||||
DeviceAuthorizationEndpoint() *Endpoint
|
||||
CheckSessionIframe() *Endpoint
|
||||
|
||||
AuthMethodPostSupported() bool
|
||||
CodeMethodS256Supported() bool
|
||||
|
|
|
@ -9,12 +9,12 @@ import (
|
|||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
strs "github.com/zitadel/oidc/v3/pkg/strings"
|
||||
)
|
||||
|
||||
type DeviceAuthorizationConfig struct {
|
||||
|
@ -276,7 +276,7 @@ func (r *DeviceAuthorizationState) GetAMR() []string {
|
|||
}
|
||||
|
||||
func (r *DeviceAuthorizationState) GetAudience() []string {
|
||||
if !strs.Contains(r.Audience, r.ClientID) {
|
||||
if !slices.Contains(r.Audience, r.ClientID) {
|
||||
r.Audience = append(r.Audience, r.ClientID)
|
||||
}
|
||||
return r.Audience
|
||||
|
@ -344,10 +344,11 @@ func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, c
|
|||
RefreshToken: refreshToken,
|
||||
TokenType: oidc.BearerToken,
|
||||
ExpiresIn: uint64(validity.Seconds()),
|
||||
Scope: tokenRequest.GetScopes(),
|
||||
}
|
||||
|
||||
// TODO(v4): remove type assertion
|
||||
if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && strs.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) {
|
||||
if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && slices.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) {
|
||||
response.IDToken, err = CreateIDToken(ctx, IssuerFromContext(ctx), idTokenRequest, client.IDTokenLifetime(), accessToken, "", creator.Storage(), client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -45,6 +45,7 @@ func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage Di
|
|||
EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer),
|
||||
JwksURI: config.KeysEndpoint().Absolute(issuer),
|
||||
DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer),
|
||||
CheckSessionIframe: config.CheckSessionIframe().Absolute(issuer),
|
||||
ScopesSupported: Scopes(config),
|
||||
ResponseTypesSupported: ResponseTypes(config),
|
||||
GrantTypesSupported: GrantTypes(config),
|
||||
|
@ -100,7 +101,11 @@ func createDiscoveryConfigV2(ctx context.Context, config Configuration, storage
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -135,7 +140,7 @@ func GrantTypes(c Configuration) []oidc.GrantType {
|
|||
}
|
||||
|
||||
func SubjectTypes(c Configuration) []string {
|
||||
return []string{"public"} //TODO: config
|
||||
return []string{"public"} // TODO: config
|
||||
}
|
||||
|
||||
func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string {
|
||||
|
|
|
@ -81,6 +81,11 @@ func Test_scopes(t *testing.T) {
|
|||
args{},
|
||||
op.DefaultSupportedScopes,
|
||||
},
|
||||
{
|
||||
"custom scopes",
|
||||
args{newTestProvider(&op.Config{SupportedScopes: []string{"test1", "test2"}})},
|
||||
[]string{"test1", "test2"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -46,6 +46,12 @@ func AuthRequestError(w http.ResponseWriter, r *http.Request, authReq ErrAuthReq
|
|||
return
|
||||
}
|
||||
e.State = authReq.GetState()
|
||||
var sessionState string
|
||||
authRequestSessionState, ok := authReq.(AuthRequestSessionState)
|
||||
if ok {
|
||||
sessionState = authRequestSessionState.GetSessionState()
|
||||
}
|
||||
e.SessionState = sessionState
|
||||
var responseMode oidc.ResponseMode
|
||||
if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok {
|
||||
responseMode = rm.GetResponseMode()
|
||||
|
@ -92,6 +98,12 @@ func TryErrorRedirect(ctx context.Context, authReq ErrAuthRequest, parent error,
|
|||
}
|
||||
|
||||
e.State = authReq.GetState()
|
||||
var sessionState string
|
||||
authRequestSessionState, ok := authReq.(AuthRequestSessionState)
|
||||
if ok {
|
||||
sessionState = authRequestSessionState.GetSessionState()
|
||||
}
|
||||
e.SessionState = sessionState
|
||||
var responseMode oidc.ResponseMode
|
||||
if rm, ok := authReq.(interface{ GetResponseMode() oidc.ResponseMode }); ok {
|
||||
responseMode = rm.GetResponseMode()
|
||||
|
|
|
@ -106,6 +106,20 @@ func (mr *MockConfigurationMockRecorder) BackChannelLogoutSupported() *gomock.Ca
|
|||
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.
|
||||
func (m *MockConfiguration) CodeMethodS256Supported() bool {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -167,6 +167,7 @@ type Config struct {
|
|||
RequestObjectSupported bool
|
||||
SupportedUILocales []language.Tag
|
||||
SupportedClaims []string
|
||||
SupportedScopes []string
|
||||
DeviceAuthorization DeviceAuthorizationConfig
|
||||
BackChannelLogoutSupported bool
|
||||
BackChannelLogoutSessionSupported bool
|
||||
|
@ -338,6 +339,10 @@ func (o *Provider) DeviceAuthorizationEndpoint() *Endpoint {
|
|||
return o.endpoints.DeviceAuthorization
|
||||
}
|
||||
|
||||
func (o *Provider) CheckSessionIframe() *Endpoint {
|
||||
return o.endpoints.CheckSessionIframe
|
||||
}
|
||||
|
||||
func (o *Provider) KeysEndpoint() *Endpoint {
|
||||
return o.endpoints.JwksURI
|
||||
}
|
||||
|
|
|
@ -232,7 +232,7 @@ func TestRoutes(t *testing.T) {
|
|||
"scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(),
|
||||
},
|
||||
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
|
||||
|
|
|
@ -145,7 +145,7 @@ func TestServerRoutes(t *testing.T) {
|
|||
"assertion": jwtProfileToken,
|
||||
},
|
||||
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",
|
||||
|
@ -174,7 +174,7 @@ func TestServerRoutes(t *testing.T) {
|
|||
"scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.String(),
|
||||
},
|
||||
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
|
||||
|
|
|
@ -2,11 +2,11 @@ package op
|
|||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/strings"
|
||||
)
|
||||
|
||||
type TokenCreator interface {
|
||||
|
@ -65,6 +65,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli
|
|||
TokenType: oidc.BearerToken,
|
||||
ExpiresIn: exp,
|
||||
State: state,
|
||||
Scope: request.GetScopes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -82,13 +83,13 @@ func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storag
|
|||
func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool {
|
||||
switch req := tokenRequest.(type) {
|
||||
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:
|
||||
return req.GetRequestedTokenType() == oidc.RefreshTokenType
|
||||
case RefreshTokenRequest:
|
||||
return true
|
||||
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:
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -120,5 +120,6 @@ func CreateClientCredentialsTokenResponse(ctx context.Context, tokenRequest Toke
|
|||
AccessToken: accessToken,
|
||||
TokenType: oidc.BearerToken,
|
||||
ExpiresIn: uint64(validity.Seconds()),
|
||||
Scope: tokenRequest.GetScopes(),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, crea
|
|||
AccessToken: accessToken,
|
||||
TokenType: oidc.BearerToken,
|
||||
ExpiresIn: uint64(validity.Seconds()),
|
||||
Scope: tokenRequest.GetScopes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/strings"
|
||||
)
|
||||
|
||||
type RefreshTokenRequest interface {
|
||||
|
@ -85,7 +85,7 @@ func ValidateRefreshTokenScopes(requestedScopes []string, authRequest RefreshTok
|
|||
return nil
|
||||
}
|
||||
for _, scope := range requestedScopes {
|
||||
if !strings.Contains(authRequest.GetScopes(), scope) {
|
||||
if !slices.Contains(authRequest.GetScopes(), scope) {
|
||||
return oidc.ErrInvalidScope()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package strings
|
||||
|
||||
import "slices"
|
||||
|
||||
// Deprecated: Use standard library [slices.Contains] instead.
|
||||
func Contains(list []string, needle string) bool {
|
||||
for _, item := range list {
|
||||
if item == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// TODO(v4): remove package.
|
||||
return slices.Contains(list, needle)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue