Merge branch 'zitadel:main' into main
This commit is contained in:
commit
b6bfbf78d1
48 changed files with 886 additions and 301 deletions
4
.github/workflows/issue.yml
vendored
4
.github/workflows/issue.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: add issue
|
- name: add issue
|
||||||
uses: actions/add-to-project@v1.0.1
|
uses: actions/add-to-project@v1.0.2
|
||||||
if: ${{ github.event_name == 'issues' }}
|
if: ${{ github.event_name == 'issues' }}
|
||||||
with:
|
with:
|
||||||
# You can target a repository in a different organization
|
# You can target a repository in a different organization
|
||||||
|
@ -28,7 +28,7 @@ jobs:
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
||||||
- name: add pr
|
- name: add pr
|
||||||
uses: actions/add-to-project@v1.0.1
|
uses: actions/add-to-project@v1.0.2
|
||||||
if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}}
|
if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}}
|
||||||
with:
|
with:
|
||||||
# You can target a repository in a different organization
|
# You can target a repository in a different organization
|
||||||
|
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -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.3.1
|
- uses: codecov/codecov-action@v5.3.1
|
||||||
with:
|
with:
|
||||||
file: ./profile.cov
|
file: ./profile.cov
|
||||||
name: codecov-go
|
name: codecov-go
|
||||||
|
|
66
README.md
66
README.md
|
@ -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,20 +60,54 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`)
|
> Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`)
|
||||||
|
|
||||||
|
### Server configuration
|
||||||
|
|
||||||
|
Example server allows extra configuration using environment variables and could be used for end to
|
||||||
|
end testing of your services.
|
||||||
|
|
||||||
|
| Name | Format | Description |
|
||||||
|
| ------------ | -------------------------------- | ------------------------------------- |
|
||||||
|
| PORT | Number between 1 and 65535 | OIDC listen port |
|
||||||
|
| REDIRECT_URI | Comma-separated URIs | List of allowed redirect URIs |
|
||||||
|
| USERS_FILE | Path to json in local filesystem | Users with their data and credentials |
|
||||||
|
|
||||||
|
Here is json equivalent for one of the default users
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id2": {
|
||||||
|
"ID": "id2",
|
||||||
|
"Username": "test-user2",
|
||||||
|
"Password": "verysecure",
|
||||||
|
"FirstName": "Test",
|
||||||
|
"LastName": "User2",
|
||||||
|
"Email": "test-user2@zitadel.ch",
|
||||||
|
"EmailVerified": true,
|
||||||
|
"Phone": "",
|
||||||
|
"PhoneVerified": false,
|
||||||
|
"PreferredLanguage": "DE",
|
||||||
|
"IsAdmin": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
| | Relying party | OpenID Provider | Specification |
|
| | Relying party | OpenID Provider | Specification |
|
||||||
| -------------------- | ------------- | --------------- | ----------------------------------------- |
|
| -------------------- | ------------- | --------------- | -------------------------------------------- |
|
||||||
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] |
|
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] |
|
||||||
| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] |
|
| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] |
|
||||||
| Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] |
|
| Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] |
|
||||||
|
@ -85,18 +119,20 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid
|
||||||
| Token Exchange | yes | yes | [RFC 8693][9] |
|
| Token Exchange | yes | yes | [RFC 8693][9] |
|
||||||
| Device Authorization | yes | yes | [RFC 8628][10] |
|
| Device Authorization | yes | yes | [RFC 8628][10] |
|
||||||
| mTLS | not yet | not yet | [RFC 8705][11] |
|
| mTLS | not yet | not yet | [RFC 8705][11] |
|
||||||
|
| Back-Channel Logout | not yet | yes | OpenID Connect [Back-Channel Logout][12] 1.0 |
|
||||||
|
|
||||||
[1]: <https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth> "3.1. Authentication using the Authorization Code Flow"
|
[1]: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth "3.1. Authentication using the Authorization Code Flow"
|
||||||
[2]: <https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth> "3.2. Authentication using the Implicit Flow"
|
[2]: https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth "3.2. Authentication using the Implicit Flow"
|
||||||
[3]: <https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth> "3.3. Authentication using the Hybrid Flow"
|
[3]: https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth "3.3. Authentication using the Hybrid Flow"
|
||||||
[4]: <https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication> "9. Client Authentication"
|
[4]: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication "9. Client Authentication"
|
||||||
[5]: <https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens> "12. Using Refresh Tokens"
|
[5]: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens "12. Using Refresh Tokens"
|
||||||
[6]: <https://openid.net/specs/openid-connect-discovery-1_0.html> "OpenID Connect Discovery 1.0 incorporating errata set 1"
|
[6]: https://openid.net/specs/openid-connect-discovery-1_0.html "OpenID Connect Discovery 1.0 incorporating errata set 1"
|
||||||
[7]: <https://www.rfc-editor.org/rfc/rfc7523.html> "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
|
[7]: https://www.rfc-editor.org/rfc/rfc7523.html "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants"
|
||||||
[8]: <https://www.rfc-editor.org/rfc/rfc7636.html> "Proof Key for Code Exchange by OAuth Public Clients"
|
[8]: https://www.rfc-editor.org/rfc/rfc7636.html "Proof Key for Code Exchange by OAuth Public Clients"
|
||||||
[9]: <https://www.rfc-editor.org/rfc/rfc8693.html> "OAuth 2.0 Token Exchange"
|
[9]: https://www.rfc-editor.org/rfc/rfc8693.html "OAuth 2.0 Token Exchange"
|
||||||
[10]: <https://www.rfc-editor.org/rfc/rfc8628.html> "OAuth 2.0 Device Authorization Grant"
|
[10]: https://www.rfc-editor.org/rfc/rfc8628.html "OAuth 2.0 Device Authorization Grant"
|
||||||
[11]: <https://www.rfc-editor.org/rfc/rfc8705.html> "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
|
[11]: https://www.rfc-editor.org/rfc/rfc8705.html "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
|
||||||
|
[12]: https://openid.net/specs/openid-connect-backchannel-1_0.html "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1"
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
|
@ -121,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
|
||||||
|
|
||||||
|
@ -153,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
|
||||||
|
|
|
@ -56,6 +56,7 @@ func main() {
|
||||||
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
|
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
|
||||||
rp.WithHTTPClient(client),
|
rp.WithHTTPClient(client),
|
||||||
rp.WithLogger(logger),
|
rp.WithLogger(logger),
|
||||||
|
rp.WithSigningAlgsFromDiscovery(),
|
||||||
}
|
}
|
||||||
if clientSecret == "" {
|
if clientSecret == "" {
|
||||||
options = append(options, rp.WithPKCE(cookieHandler))
|
options = append(options, rp.WithPKCE(cookieHandler))
|
||||||
|
@ -108,6 +109,7 @@ func main() {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
40
example/server/config/config.go
Normal file
40
example/server/config/config.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// default port for the http server to run
|
||||||
|
DefaultIssuerPort = "9998"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port string
|
||||||
|
RedirectURI []string
|
||||||
|
UsersFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromEnvVars loads configuration parameters from environment variables.
|
||||||
|
// If there is no such variable defined, then use default values.
|
||||||
|
func FromEnvVars(defaults *Config) *Config {
|
||||||
|
if defaults == nil {
|
||||||
|
defaults = &Config{}
|
||||||
|
}
|
||||||
|
cfg := &Config{
|
||||||
|
Port: defaults.Port,
|
||||||
|
RedirectURI: defaults.RedirectURI,
|
||||||
|
UsersFile: defaults.UsersFile,
|
||||||
|
}
|
||||||
|
if value, ok := os.LookupEnv("PORT"); ok {
|
||||||
|
cfg.Port = value
|
||||||
|
}
|
||||||
|
if value, ok := os.LookupEnv("USERS_FILE"); ok {
|
||||||
|
cfg.UsersFile = value
|
||||||
|
}
|
||||||
|
if value, ok := os.LookupEnv("REDIRECT_URI"); ok {
|
||||||
|
cfg.RedirectURI = strings.Split(value, ",")
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
77
example/server/config/config_test.go
Normal file
77
example/server/config/config_test.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFromEnvVars(t *testing.T) {
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
env map[string]string
|
||||||
|
defaults *Config
|
||||||
|
want *Config
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no vars, no default values",
|
||||||
|
env: map[string]string{},
|
||||||
|
want: &Config{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no vars, only defaults",
|
||||||
|
env: map[string]string{},
|
||||||
|
defaults: &Config{
|
||||||
|
Port: "6666",
|
||||||
|
UsersFile: "/default/user/path",
|
||||||
|
RedirectURI: []string{"re", "direct", "uris"},
|
||||||
|
},
|
||||||
|
want: &Config{
|
||||||
|
Port: "6666",
|
||||||
|
UsersFile: "/default/user/path",
|
||||||
|
RedirectURI: []string{"re", "direct", "uris"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "overriding default values",
|
||||||
|
env: map[string]string{
|
||||||
|
"PORT": "1234",
|
||||||
|
"USERS_FILE": "/path/to/users",
|
||||||
|
"REDIRECT_URI": "http://redirect/redirect",
|
||||||
|
},
|
||||||
|
defaults: &Config{
|
||||||
|
Port: "6666",
|
||||||
|
UsersFile: "/default/user/path",
|
||||||
|
RedirectURI: []string{"re", "direct", "uris"},
|
||||||
|
},
|
||||||
|
want: &Config{
|
||||||
|
Port: "1234",
|
||||||
|
UsersFile: "/path/to/users",
|
||||||
|
RedirectURI: []string{"http://redirect/redirect"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple redirect uris",
|
||||||
|
env: map[string]string{
|
||||||
|
"REDIRECT_URI": "http://host_1,http://host_2,http://host_3",
|
||||||
|
},
|
||||||
|
want: &Config{
|
||||||
|
RedirectURI: []string{
|
||||||
|
"http://host_1", "http://host_2", "http://host_3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
for k, v := range tc.env {
|
||||||
|
os.Setenv(k, v)
|
||||||
|
}
|
||||||
|
cfg := FromEnvVars(tc.defaults)
|
||||||
|
if fmt.Sprint(cfg) != fmt.Sprint(tc.want) {
|
||||||
|
t.Errorf("Expected FromEnvVars()=%q, but got %q", tc.want, cfg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/v3/example/server/storage"
|
|
||||||
"github.com/zitadel/oidc/v3/pkg/op"
|
"github.com/zitadel/oidc/v3/pkg/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,14 +19,6 @@ const (
|
||||||
pathLoggedOut = "/logged-out"
|
pathLoggedOut = "/logged-out"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
storage.RegisterClients(
|
|
||||||
storage.NativeClient("native"),
|
|
||||||
storage.WebClient("web", "secret"),
|
|
||||||
storage.WebClient("api", "secret"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
op.Storage
|
op.Storage
|
||||||
authenticate
|
authenticate
|
||||||
|
|
|
@ -6,36 +6,53 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v3/example/server/config"
|
||||||
"github.com/zitadel/oidc/v3/example/server/exampleop"
|
"github.com/zitadel/oidc/v3/example/server/exampleop"
|
||||||
"github.com/zitadel/oidc/v3/example/server/storage"
|
"github.com/zitadel/oidc/v3/example/server/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func getUserStore(cfg *config.Config) (storage.UserStore, error) {
|
||||||
|
if cfg.UsersFile == "" {
|
||||||
|
return storage.NewUserStore(fmt.Sprintf("http://localhost:%s/", cfg.Port)), nil
|
||||||
|
}
|
||||||
|
return storage.StoreFromFile(cfg.UsersFile)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
//we will run on :9998
|
cfg := config.FromEnvVars(&config.Config{Port: "9998"})
|
||||||
port := "9998"
|
|
||||||
//which gives us the issuer: http://localhost:9998/
|
|
||||||
issuer := fmt.Sprintf("http://localhost:%s/", port)
|
|
||||||
|
|
||||||
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
|
||||||
// this might be the layer for accessing your database
|
|
||||||
// in this example it will be handled in-memory
|
|
||||||
storage := storage.NewStorage(storage.NewUserStore(issuer))
|
|
||||||
|
|
||||||
logger := slog.New(
|
logger := slog.New(
|
||||||
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
AddSource: true,
|
AddSource: true,
|
||||||
Level: slog.LevelDebug,
|
Level: slog.LevelDebug,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//which gives us the issuer: http://localhost:9998/
|
||||||
|
issuer := fmt.Sprintf("http://localhost:%s/", cfg.Port)
|
||||||
|
|
||||||
|
storage.RegisterClients(
|
||||||
|
storage.NativeClient("native", cfg.RedirectURI...),
|
||||||
|
storage.WebClient("web", "secret", cfg.RedirectURI...),
|
||||||
|
storage.WebClient("api", "secret", cfg.RedirectURI...),
|
||||||
|
)
|
||||||
|
|
||||||
|
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
||||||
|
// this might be the layer for accessing your database
|
||||||
|
// in this example it will be handled in-memory
|
||||||
|
store, err := getUserStore(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("cannot create UserStore", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
storage := storage.NewStorage(store)
|
||||||
router := exampleop.SetupServer(issuer, storage, logger, false)
|
router := exampleop.SetupServer(issuer, storage, logger, false)
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: ":" + port,
|
Addr: ":" + cfg.Port,
|
||||||
Handler: router,
|
Handler: router,
|
||||||
}
|
}
|
||||||
logger.Info("server listening, press ctrl+c to stop", "addr", fmt.Sprintf("http://localhost:%s/", port))
|
logger.Info("server listening, press ctrl+c to stop", "addr", issuer)
|
||||||
err := server.ListenAndServe()
|
if server.ListenAndServe() != http.ErrServerClosed {
|
||||||
if err != http.ErrServerClosed {
|
|
||||||
logger.Error("server terminated", "error", err)
|
logger.Error("server terminated", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,7 +121,7 @@ func (a *AuthRequest) Done() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
|
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
|
||||||
prompts := make([]string, len(oidcPrompt))
|
prompts := make([]string, 0, len(oidcPrompt))
|
||||||
for _, oidcPrompt := range oidcPrompt {
|
for _, oidcPrompt := range oidcPrompt {
|
||||||
switch oidcPrompt {
|
switch oidcPrompt {
|
||||||
case oidc.PromptNone,
|
case oidc.PromptNone,
|
||||||
|
|
|
@ -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")
|
||||||
|
@ -385,14 +388,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 +486,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,12 +595,17 @@ 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
|
||||||
|
//
|
||||||
|
// [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 string) (string, string, error) {
|
func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
|
@ -607,18 +613,21 @@ func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string,
|
||||||
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()
|
token := uuid.NewString()
|
||||||
refreshToken.Token = token
|
refreshToken.Token = token
|
||||||
refreshToken.ID = token
|
refreshToken.ID = token
|
||||||
|
refreshToken.Expiration = time.Now().Add(5 * time.Hour)
|
||||||
s.refreshTokens[token] = refreshToken
|
s.refreshTokens[token] = refreshToken
|
||||||
return token, refreshToken.ID, nil
|
return token, refreshToken.ID, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
@ -35,6 +37,18 @@ type userStore struct {
|
||||||
users map[string]*User
|
users map[string]*User
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StoreFromFile(path string) (UserStore, error) {
|
||||||
|
users := map[string]*User{}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &users); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return userStore{users}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewUserStore(issuer string) UserStore {
|
func NewUserStore(issuer string) UserStore {
|
||||||
hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
|
hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
|
||||||
return userStore{
|
return userStore{
|
||||||
|
|
70
example/server/storage/user_test.go
Normal file
70
example/server/storage/user_test.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStoreFromFile(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
pathToFile string
|
||||||
|
content string
|
||||||
|
want UserStore
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal user file",
|
||||||
|
pathToFile: "userfile.json",
|
||||||
|
content: `{
|
||||||
|
"id1": {
|
||||||
|
"ID": "id1",
|
||||||
|
"EmailVerified": true,
|
||||||
|
"PreferredLanguage": "DE"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
want: userStore{map[string]*User{
|
||||||
|
"id1": {
|
||||||
|
ID: "id1",
|
||||||
|
EmailVerified: true,
|
||||||
|
PreferredLanguage: language.German,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "malformed file",
|
||||||
|
pathToFile: "whatever",
|
||||||
|
content: "not a json just a text",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not existing file",
|
||||||
|
pathToFile: "what/ever/file",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
actualPath := path.Join(t.TempDir(), tc.pathToFile)
|
||||||
|
|
||||||
|
if tc.content != "" && tc.pathToFile != "" {
|
||||||
|
if err := os.WriteFile(actualPath, []byte(tc.content), 0666); err != nil {
|
||||||
|
t.Fatalf("cannot create file with test content: %q", tc.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, err := StoreFromFile(actualPath)
|
||||||
|
if err != nil && !tc.wantErr {
|
||||||
|
t.Errorf("StoreFromFile(%q) returned unexpected error %q", tc.pathToFile, err)
|
||||||
|
} else if err == nil && tc.wantErr {
|
||||||
|
t.Errorf("StoreFromFile(%q) did not return an expected error", tc.pathToFile)
|
||||||
|
}
|
||||||
|
if !tc.wantErr && !reflect.DeepEqual(tc.want, result.(userStore)) {
|
||||||
|
t.Errorf("expected StoreFromFile(%q) = %v, but got %v",
|
||||||
|
tc.pathToFile, tc.want, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
32
go.mod
32
go.mod
|
@ -3,36 +3,36 @@ module github.com/zitadel/oidc/v3
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||||
github.com/go-chi/chi/v5 v5.0.12
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2
|
github.com/go-jose/go-jose/v4 v4.0.4
|
||||||
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.0
|
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.0
|
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.26.0
|
go.opentelemetry.io/otel v1.29.0
|
||||||
golang.org/x/oauth2 v0.20.0
|
golang.org/x/oauth2 v0.26.0
|
||||||
golang.org/x/text v0.15.0
|
golang.org/x/text v0.22.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.26.0 // indirect
|
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.26.0 // indirect
|
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||||
golang.org/x/crypto v0.22.0 // indirect
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
golang.org/x/net v0.23.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.19.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
64
go.sum
64
go.sum
|
@ -1,15 +1,15 @@
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.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.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
github.com/go-chi/chi/v5 v5.0.12/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.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||||
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.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
|
@ -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=
|
||||||
|
@ -41,40 +41,40 @@ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/
|
||||||
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
|
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.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.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank=
|
github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y=
|
||||||
github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
|
github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
|
||||||
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
|
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
|
||||||
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
|
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
|
||||||
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
|
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||||
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
|
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
||||||
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
|
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||||
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
|
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.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
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.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
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.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||||
golang.org/x/oauth2 v0.20.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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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=
|
||||||
|
|
|
@ -12,11 +12,12 @@ import (
|
||||||
|
|
||||||
"github.com/go-jose/go-jose/v4"
|
"github.com/go-jose/go-jose/v4"
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||||
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"
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -41,7 +42,7 @@ func Discover(ctx context.Context, issuer string, httpClient *http.Client, wellK
|
||||||
discoveryConfig := new(oidc.DiscoveryConfiguration)
|
discoveryConfig := new(oidc.DiscoveryConfiguration)
|
||||||
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
|
err = httphelper.HttpRequest(httpClient, req, &discoveryConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Join(oidc.ErrDiscoveryFailed, err)
|
||||||
}
|
}
|
||||||
if logger, ok := logging.FromContext(ctx); ok {
|
if logger, ok := logging.FromContext(ctx); ok {
|
||||||
logger.Debug("discover", "config", discoveryConfig)
|
logger.Debug("discover", "config", discoveryConfig)
|
||||||
|
@ -196,12 +197,12 @@ func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, cal
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
|
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
|
||||||
privateKey, err := crypto.BytesToPrivateKey(key)
|
privateKey, algorithm, err := crypto.BytesToPrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
signingKey := jose.SigningKey{
|
signingKey := jose.SigningKey{
|
||||||
Algorithm: jose.RS256,
|
Algorithm: algorithm,
|
||||||
Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID},
|
Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID},
|
||||||
}
|
}
|
||||||
return jose.NewSigner(signingKey, &jose.SignerOptions{})
|
return jose.NewSigner(signingKey, &jose.SignerOptions{})
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDiscover(t *testing.T) {
|
func TestDiscover(t *testing.T) {
|
||||||
|
@ -22,7 +23,7 @@ func TestDiscover(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
wantFields *wantFields
|
wantFields *wantFields
|
||||||
wantErr bool
|
wantErr error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "spotify", // https://github.com/zitadel/oidc/issues/406
|
name: "spotify", // https://github.com/zitadel/oidc/issues/406
|
||||||
|
@ -32,17 +33,20 @@ func TestDiscover(t *testing.T) {
|
||||||
wantFields: &wantFields{
|
wantFields: &wantFields{
|
||||||
UILocalesSupported: true,
|
UILocalesSupported: true,
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discovery failed",
|
||||||
|
args: args{
|
||||||
|
issuer: "https://example.com",
|
||||||
|
},
|
||||||
|
wantErr: oidc.ErrDiscoveryFailed,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := Discover(context.Background(), tt.args.issuer, http.DefaultClient, tt.args.wellKnownUrl...)
|
got, err := Discover(context.Background(), tt.args.issuer, http.DefaultClient, tt.args.wellKnownUrl...)
|
||||||
if tt.wantErr {
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
if tt.wantFields == nil {
|
if tt.wantFields == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -541,7 +541,7 @@ func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp R
|
||||||
rp.CookieHandler().DeleteCookie(w, pkceCode)
|
rp.CookieHandler().DeleteCookie(w, pkceCode)
|
||||||
}
|
}
|
||||||
if rp.Signer() != nil {
|
if rp.Signer() != nil {
|
||||||
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer())
|
assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer(), rp.OAuthConfig().Endpoint.TokenURL}, time.Hour, rp.Signer())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
unauthorizedError(w, r, "failed to build assertion: "+err.Error(), state, rp)
|
unauthorizedError(w, r, "failed to build assertion: "+err.Error(), state, rp)
|
||||||
return
|
return
|
||||||
|
|
|
@ -21,6 +21,14 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) {
|
||||||
return sha512.New384(), nil
|
return sha512.New384(), nil
|
||||||
case jose.RS512, jose.ES512, jose.PS512:
|
case jose.RS512, jose.ES512, jose.PS512:
|
||||||
return sha512.New(), nil
|
return sha512.New(), nil
|
||||||
|
|
||||||
|
// There is no published spec for this yet, but we have confirmation it will get published.
|
||||||
|
// There is consensus here: https://bitbucket.org/openid/connect/issues/1125/_hash-algorithm-for-eddsa-id-tokens
|
||||||
|
// Currently Go and go-jose only supports the ed25519 curve key for EdDSA, so we can safely assume sha512 here.
|
||||||
|
// It is unlikely ed448 will ever be supported: https://github.com/golang/go/issues/29390
|
||||||
|
case jose.EdDSA:
|
||||||
|
return sha512.New(), nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm)
|
return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,45 @@
|
||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BytesToPrivateKey(b []byte) (*rsa.PrivateKey, error) {
|
var (
|
||||||
|
ErrPEMDecode = errors.New("PEM decode failed")
|
||||||
|
ErrUnsupportedFormat = errors.New("key is neither in PKCS#1 nor PKCS#8 format")
|
||||||
|
ErrUnsupportedPrivateKey = errors.New("unsupported key type, must be RSA, ECDSA or ED25519 private key")
|
||||||
|
)
|
||||||
|
|
||||||
|
func BytesToPrivateKey(b []byte) (crypto.PublicKey, jose.SignatureAlgorithm, error) {
|
||||||
block, _ := pem.Decode(b)
|
block, _ := pem.Decode(b)
|
||||||
if block == nil {
|
if block == nil {
|
||||||
return nil, errors.New("PEM decode failed")
|
return nil, "", ErrPEMDecode
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err == nil {
|
||||||
|
return privateKey, jose.RS256, nil
|
||||||
|
}
|
||||||
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", ErrUnsupportedFormat
|
||||||
|
}
|
||||||
|
switch privateKey := key.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return privateKey, jose.RS256, nil
|
||||||
|
case ed25519.PrivateKey:
|
||||||
|
return privateKey, jose.EdDSA, nil
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
return privateKey, jose.ES256, nil
|
||||||
|
default:
|
||||||
|
return nil, "", ErrUnsupportedPrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
return key, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,64 @@
|
||||||
package crypto_test
|
package crypto_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rsa"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
zcrypto "github.com/zitadel/oidc/v3/pkg/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBytesToPrivateKey(tt *testing.T) {
|
func TestBytesToPrivateKey(t *testing.T) {
|
||||||
tt.Run("PEMDecodeError", func(t *testing.T) {
|
type args struct {
|
||||||
_, err := crypto.BytesToPrivateKey([]byte("The non-PEM sequence"))
|
key []byte
|
||||||
assert.EqualError(t, err, "PEM decode failed")
|
}
|
||||||
})
|
type want struct {
|
||||||
|
key crypto.Signer
|
||||||
tt.Run("InvalidKeyFormat", func(t *testing.T) {
|
algorithm jose.SignatureAlgorithm
|
||||||
_, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN PRIVATE KEY-----
|
err error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want want
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "PEMDecodeError",
|
||||||
|
args: args{
|
||||||
|
key: []byte("The non-PEM sequence"),
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
err: zcrypto.ErrPEMDecode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PKCS#1 RSA",
|
||||||
|
args: args{
|
||||||
|
key: []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
|
||||||
|
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
|
||||||
|
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
|
||||||
|
TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7
|
||||||
|
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
|
||||||
|
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
|
||||||
|
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
|
||||||
|
-----END RSA PRIVATE KEY-----`),
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
key: &rsa.PrivateKey{},
|
||||||
|
algorithm: jose.RS256,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PKCS#8 RSA",
|
||||||
|
args: args{
|
||||||
|
key: []byte(`-----BEGIN PRIVATE KEY-----
|
||||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfaDB7pK/fmP/I
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfaDB7pK/fmP/I
|
||||||
7IusSK8lTCBnPZghqIbVLt2QHYAMoEF1CaF4F4rxo2vl1Mt8gwsq4T3osQFZMvnL
|
7IusSK8lTCBnPZghqIbVLt2QHYAMoEF1CaF4F4rxo2vl1Mt8gwsq4T3osQFZMvnL
|
||||||
YHb7KNyUoJgTjLxJQADv2u4Q3U38heAzK5Tp4ry4MCnuyJIqAPK1GiruwEq4zQrx
|
YHb7KNyUoJgTjLxJQADv2u4Q3U38heAzK5Tp4ry4MCnuyJIqAPK1GiruwEq4zQrx
|
||||||
|
@ -42,21 +85,50 @@ srJnjF0H8oKmAY6hw+1Tm/n/b08p+RyL48TgVSE2vhUCgYA3BWpkD4PlCcn/FZsq
|
||||||
OrLFyFXI6jIaxskFtsRW1IxxIlAdZmxfB26P/2gx6VjLdxJI/RRPkJyEN2dP7CbR
|
OrLFyFXI6jIaxskFtsRW1IxxIlAdZmxfB26P/2gx6VjLdxJI/RRPkJyEN2dP7CbR
|
||||||
BDjb565dy1O9D6+UrY70Iuwjz+OcALRBBGTaiF2pLn6IhSzNI2sy/tXX8q8dBlg9
|
BDjb565dy1O9D6+UrY70Iuwjz+OcALRBBGTaiF2pLn6IhSzNI2sy/tXX8q8dBlg9
|
||||||
OFCrqT/emes3KytTPfa5NZtYeQ==
|
OFCrqT/emes3KytTPfa5NZtYeQ==
|
||||||
-----END PRIVATE KEY-----`))
|
-----END PRIVATE KEY-----`),
|
||||||
assert.EqualError(t, err, "x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)")
|
},
|
||||||
|
want: want{
|
||||||
|
key: &rsa.PrivateKey{},
|
||||||
|
algorithm: jose.RS256,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PKCS#8 ECDSA",
|
||||||
|
args: args{
|
||||||
|
key: []byte(`-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwwOZSU4GlP7ps/Wp
|
||||||
|
V6o0qRwxultdfYo/uUuj48QZjSuhRANCAATMiI2Han+ABKmrk5CNlxRAGC61w4d3
|
||||||
|
G4TAeuBpyzqJ7x/6NjCxoQzJzZHtNjIfjVATI59XFZWF59GhtSZbShAr
|
||||||
|
-----END PRIVATE KEY-----`),
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
key: &ecdsa.PrivateKey{},
|
||||||
|
algorithm: jose.ES256,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PKCS#8 ED25519",
|
||||||
|
args: args{
|
||||||
|
key: []byte(`-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIHu6ZtDsjjauMasBxnS9Fg87UJwKfcT/oiq6S0ktbky8
|
||||||
|
-----END PRIVATE KEY-----`),
|
||||||
|
},
|
||||||
|
want: want{
|
||||||
|
key: ed25519.PrivateKey{},
|
||||||
|
algorithm: jose.EdDSA,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
key, algorithm, err := zcrypto.BytesToPrivateKey(tt.args.key)
|
||||||
|
assert.IsType(t, tt.want.key, key)
|
||||||
|
assert.Equal(t, tt.want.algorithm, algorithm)
|
||||||
|
assert.ErrorIs(t, tt.want.err, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
tt.Run("Ok", func(t *testing.T) {
|
}
|
||||||
key, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
|
|
||||||
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
|
|
||||||
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
|
|
||||||
TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7
|
|
||||||
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
|
|
||||||
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
|
|
||||||
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
|
|
||||||
-----END RSA PRIVATE KEY-----`))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, key)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,6 +145,14 @@ type DiscoveryConfiguration struct {
|
||||||
|
|
||||||
// OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service.
|
// OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service.
|
||||||
OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
|
OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
|
||||||
|
|
||||||
|
// BackChannelLogoutSupported specifies whether the OP supports back-channel logout (https://openid.net/specs/openid-connect-backchannel-1_0.html),
|
||||||
|
// with true indicating support. If omitted, the default value is false.
|
||||||
|
BackChannelLogoutSupported bool `json:"backchannel_logout_supported,omitempty"`
|
||||||
|
|
||||||
|
// BackChannelLogoutSessionSupported specifies whether the OP can pass a sid (session ID) Claim in the Logout Token to identify the RP session with the OP.
|
||||||
|
// If supported, the sid Claim is also included in ID Tokens issued by the OP. If omitted, the default value is false.
|
||||||
|
BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthMethod string
|
type AuthMethod string
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
@ -133,6 +134,24 @@ type Error struct {
|
||||||
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"`
|
||||||
redirectDisabled bool `schema:"-"`
|
redirectDisabled bool `schema:"-"`
|
||||||
|
returnParent bool `schema:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) MarshalJSON() ([]byte, error) {
|
||||||
|
m := struct {
|
||||||
|
Error errorType `json:"error"`
|
||||||
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
|
State string `json:"state,omitempty"`
|
||||||
|
Parent string `json:"parent,omitempty"`
|
||||||
|
}{
|
||||||
|
Error: e.ErrorType,
|
||||||
|
ErrorDescription: e.Description,
|
||||||
|
State: e.State,
|
||||||
|
}
|
||||||
|
if e.returnParent {
|
||||||
|
m.Parent = e.Parent.Error()
|
||||||
|
}
|
||||||
|
return json.Marshal(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
func (e *Error) Error() string {
|
||||||
|
@ -165,6 +184,18 @@ func (e *Error) WithParent(err error) *Error {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithReturnParentToClient allows returning the set parent error to the HTTP client.
|
||||||
|
// Currently it only supports setting the parent inside JSON responses, not redirect URLs.
|
||||||
|
// As Go errors don't unmarshal well, only the marshaller is implemented for the moment.
|
||||||
|
//
|
||||||
|
// Warning: parent errors may contain sensitive data or unwanted details about the server status.
|
||||||
|
// Also, the `parent` field is not a standard error field and might confuse certain clients
|
||||||
|
// that require fully compliant responses.
|
||||||
|
func (e *Error) WithReturnParentToClient(b bool) *Error {
|
||||||
|
e.returnParent = b
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Error) WithDescription(desc string, args ...any) *Error {
|
func (e *Error) WithDescription(desc string, args ...any) *Error {
|
||||||
e.Description = fmt.Sprintf(desc, args...)
|
e.Description = fmt.Sprintf(desc, args...)
|
||||||
return e
|
return e
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultToServerError(t *testing.T) {
|
func TestDefaultToServerError(t *testing.T) {
|
||||||
|
@ -151,3 +154,39 @@ func TestError_LogValue(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestError_MarshalJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
e *Error
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple error",
|
||||||
|
e: ErrAccessDenied(),
|
||||||
|
want: `{"error":"access_denied","error_description":"The authorization request was denied."}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with description",
|
||||||
|
e: ErrAccessDenied().WithDescription("oops"),
|
||||||
|
want: `{"error":"access_denied","error_description":"oops"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with parent",
|
||||||
|
e: ErrServerError().WithParent(errors.New("oops")),
|
||||||
|
want: `{"error":"server_error"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with return parent",
|
||||||
|
e: ErrServerError().WithParent(errors.New("oops")).WithReturnParentToClient(true),
|
||||||
|
want: `{"error":"server_error","parent":"oops"}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := json.Marshal(tt.e)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.JSONEq(t, tt.want, string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
jose "github.com/go-jose/go-jose/v4"
|
jose "github.com/go-jose/go-jose/v4"
|
||||||
)
|
)
|
||||||
|
@ -92,17 +93,17 @@ func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (k
|
||||||
}
|
}
|
||||||
|
|
||||||
func algToKeyType(key any, alg string) bool {
|
func algToKeyType(key any, alg string) bool {
|
||||||
switch alg[0] {
|
if strings.HasPrefix(alg, "RS") || strings.HasPrefix(alg, "PS") {
|
||||||
case 'R', 'P':
|
|
||||||
_, ok := key.(*rsa.PublicKey)
|
_, ok := key.(*rsa.PublicKey)
|
||||||
return ok
|
return ok
|
||||||
case 'E':
|
}
|
||||||
|
if strings.HasPrefix(alg, "ES") {
|
||||||
_, ok := key.(*ecdsa.PublicKey)
|
_, ok := key.(*ecdsa.PublicKey)
|
||||||
return ok
|
return ok
|
||||||
case 'O':
|
|
||||||
_, ok := key.(*ed25519.PublicKey)
|
|
||||||
return ok
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
if alg == string(jose.EdDSA) {
|
||||||
|
_, ok := key.(ed25519.PublicKey)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -116,6 +117,7 @@ func NewAccessTokenClaims(issuer, subject string, audience []string, expiration
|
||||||
Expiration: FromTime(expiration),
|
Expiration: FromTime(expiration),
|
||||||
IssuedAt: FromTime(now),
|
IssuedAt: FromTime(now),
|
||||||
NotBefore: FromTime(now),
|
NotBefore: FromTime(now),
|
||||||
|
ClientID: clientID,
|
||||||
JWTID: jwtid,
|
JWTID: jwtid,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -234,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 {
|
||||||
|
@ -344,12 +347,12 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) {
|
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) {
|
||||||
privateKey, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
|
privateKey, algorithm, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
key := jose.SigningKey{
|
key := jose.SigningKey{
|
||||||
Algorithm: jose.RS256,
|
Algorithm: algorithm,
|
||||||
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
|
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
|
||||||
}
|
}
|
||||||
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
||||||
|
@ -380,3 +383,40 @@ type TokenExchangeResponse struct {
|
||||||
// if the requested_token_type was Access Token and scope contained openid.
|
// if the requested_token_type was Access Token and scope contained openid.
|
||||||
IDToken string `json:"id_token,omitempty"`
|
IDToken string `json:"id_token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogoutTokenClaims struct {
|
||||||
|
Issuer string `json:"iss,omitempty"`
|
||||||
|
Subject string `json:"sub,omitempty"`
|
||||||
|
Audience Audience `json:"aud,omitempty"`
|
||||||
|
IssuedAt Time `json:"iat,omitempty"`
|
||||||
|
Expiration Time `json:"exp,omitempty"`
|
||||||
|
JWTID string `json:"jti,omitempty"`
|
||||||
|
Events map[string]any `json:"events,omitempty"`
|
||||||
|
SessionID string `json:"sid,omitempty"`
|
||||||
|
Claims map[string]any `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ltcAlias LogoutTokenClaims
|
||||||
|
|
||||||
|
func (i *LogoutTokenClaims) MarshalJSON() ([]byte, error) {
|
||||||
|
return mergeAndMarshalClaims((*ltcAlias)(i), i.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *LogoutTokenClaims) UnmarshalJSON(data []byte) error {
|
||||||
|
return unmarshalJSONMulti(data, (*ltcAlias)(i), &i.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogoutTokenClaims(issuer, subject string, audience Audience, expiration time.Time, jwtID, sessionID string, skew time.Duration) *LogoutTokenClaims {
|
||||||
|
return &LogoutTokenClaims{
|
||||||
|
Issuer: issuer,
|
||||||
|
Subject: subject,
|
||||||
|
Audience: audience,
|
||||||
|
IssuedAt: FromTime(time.Now().Add(-skew)),
|
||||||
|
Expiration: FromTime(expiration),
|
||||||
|
JWTID: jwtID,
|
||||||
|
Events: map[string]any{
|
||||||
|
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
||||||
|
},
|
||||||
|
SessionID: sessionID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -72,10 +72,10 @@ type AccessTokenRequest struct {
|
||||||
Code string `schema:"code"`
|
Code string `schema:"code"`
|
||||||
RedirectURI string `schema:"redirect_uri"`
|
RedirectURI string `schema:"redirect_uri"`
|
||||||
ClientID string `schema:"client_id"`
|
ClientID string `schema:"client_id"`
|
||||||
ClientSecret string `schema:"client_secret"`
|
ClientSecret string `schema:"client_secret,omitempty"`
|
||||||
CodeVerifier string `schema:"code_verifier"`
|
CodeVerifier string `schema:"code_verifier,omitempty"`
|
||||||
ClientAssertion string `schema:"client_assertion"`
|
ClientAssertion string `schema:"client_assertion,omitempty"`
|
||||||
ClientAssertionType string `schema:"client_assertion_type"`
|
ClientAssertionType string `schema:"client_assertion_type,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AccessTokenRequest) GrantType() GrantType {
|
func (a *AccessTokenRequest) GrantType() GrantType {
|
||||||
|
|
|
@ -145,6 +145,7 @@ func TestNewAccessTokenClaims(t *testing.T) {
|
||||||
Subject: "hello@me.com",
|
Subject: "hello@me.com",
|
||||||
Audience: Audience{"foo"},
|
Audience: Audience{"foo"},
|
||||||
Expiration: 12345,
|
Expiration: 12345,
|
||||||
|
ClientID: "foo",
|
||||||
JWTID: "900",
|
JWTID: "900",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -241,3 +242,39 @@ func TestIDTokenClaims_GetUserInfo(t *testing.T) {
|
||||||
got := idTokenData.GetUserInfo()
|
got := idTokenData.GetUserInfo()
|
||||||
assert.Equal(t, want, got)
|
assert.Equal(t, want, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewLogoutTokenClaims(t *testing.T) {
|
||||||
|
want := &LogoutTokenClaims{
|
||||||
|
Issuer: "zitadel",
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo", "just@me.com"},
|
||||||
|
Expiration: 12345,
|
||||||
|
JWTID: "jwtID",
|
||||||
|
Events: map[string]any{
|
||||||
|
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
||||||
|
},
|
||||||
|
SessionID: "sessionID",
|
||||||
|
Claims: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
got := NewLogoutTokenClaims(
|
||||||
|
want.Issuer,
|
||||||
|
want.Subject,
|
||||||
|
want.Audience,
|
||||||
|
want.Expiration.AsTime(),
|
||||||
|
want.JWTID,
|
||||||
|
want.SessionID,
|
||||||
|
1*time.Second,
|
||||||
|
)
|
||||||
|
|
||||||
|
// test if the dynamic timestamp is around now,
|
||||||
|
// allowing for a delta of 1, just in case we flip on
|
||||||
|
// either side of a second boundry.
|
||||||
|
nowMinusSkew := NowTime() - 1
|
||||||
|
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
|
||||||
|
|
||||||
|
// Make equal not fail on dynamic timestamp
|
||||||
|
got.IssuedAt = 0
|
||||||
|
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package oidc
|
||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -78,22 +77,16 @@ func (l *Locale) MarshalJSON() ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON implements json.Unmarshaler.
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
// When [language.ValueError] is encountered, the containing tag will be set
|
// All unmarshal errors for are ignored.
|
||||||
|
// When an error is encountered, the containing tag will be set
|
||||||
// to an empty value (language "und") and no error will be returned.
|
// to an empty value (language "und") and no error will be returned.
|
||||||
// This state can be checked with the `l.Tag().IsRoot()` method.
|
// This state can be checked with the `l.Tag().IsRoot()` method.
|
||||||
func (l *Locale) UnmarshalJSON(data []byte) error {
|
func (l *Locale) UnmarshalJSON(data []byte) error {
|
||||||
err := json.Unmarshal(data, &l.tag)
|
err := json.Unmarshal(data, &l.tag)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// catch "well-formed but unknown" errors
|
|
||||||
var target language.ValueError
|
|
||||||
if errors.As(err, &target) {
|
|
||||||
l.tag = language.Tag{}
|
l.tag = language.Tag{}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Locales []language.Tag
|
type Locales []language.Tag
|
||||||
|
|
|
@ -234,7 +234,9 @@ func TestLocale_UnmarshalJSON(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "bad form, error",
|
name: "bad form, error",
|
||||||
input: `{"locale": "g!!!!!"}`,
|
input: `{"locale": "g!!!!!"}`,
|
||||||
wantErr: true,
|
want: dst{
|
||||||
|
Locale: &Locale{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -41,6 +40,7 @@ type IDClaims interface {
|
||||||
var (
|
var (
|
||||||
ErrParse = errors.New("parsing of request failed")
|
ErrParse = errors.New("parsing of request failed")
|
||||||
ErrIssuerInvalid = errors.New("issuer does not match")
|
ErrIssuerInvalid = errors.New("issuer does not match")
|
||||||
|
ErrDiscoveryFailed = errors.New("OpenID Provider Configuration Discovery has failed")
|
||||||
ErrSubjectMissing = errors.New("subject missing")
|
ErrSubjectMissing = errors.New("subject missing")
|
||||||
ErrAudience = errors.New("audience is not valid")
|
ErrAudience = errors.New("audience is not valid")
|
||||||
ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty")
|
ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty")
|
||||||
|
@ -83,7 +83,7 @@ type ACRVerifier func(string) error
|
||||||
// if none of the provided values matches the acr claim
|
// if none of the provided values matches the acr claim
|
||||||
func DefaultACRVerifier(possibleValues []string) ACRVerifier {
|
func DefaultACRVerifier(possibleValues []string) ACRVerifier {
|
||||||
return func(acr string) error {
|
return func(acr string) error {
|
||||||
if !str.Contains(possibleValues, acr) {
|
if !slices.Contains(possibleValues, acr) {
|
||||||
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
|
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -122,7 +122,7 @@ func CheckIssuer(claims Claims, issuer string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckAudience(claims Claims, clientID string) error {
|
func CheckAudience(claims Claims, clientID string) error {
|
||||||
if !str.Contains(claims.GetAudience(), clientID) {
|
if !slices.Contains(claims.GetAudience(), clientID) {
|
||||||
return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID)
|
return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,13 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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 {
|
||||||
|
@ -82,19 +82,27 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||||
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
|
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
|
||||||
err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
|
err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
AuthRequestError(w, r, authReq, err, authorizer)
|
AuthRequestError(w, r, nil, err, authorizer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if authReq.ClientID == "" {
|
if authReq.ClientID == "" {
|
||||||
AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing client_id"), authorizer)
|
AuthRequestError(w, r, nil, fmt.Errorf("auth request is missing client_id"), authorizer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if authReq.RedirectURI == "" {
|
if authReq.RedirectURI == "" {
|
||||||
AuthRequestError(w, r, authReq, fmt.Errorf("auth request is missing redirect_uri"), authorizer)
|
AuthRequestError(w, r, nil, fmt.Errorf("auth request is missing redirect_uri"), authorizer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
validation := ValidateAuthRequest
|
|
||||||
|
var client Client
|
||||||
|
validation := func(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) {
|
||||||
|
client, err = authorizer.Storage().GetClientByClientID(ctx, authReq.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return "", oidc.ErrInvalidRequestRedirectURI().WithDescription("unable to retrieve client by id").WithParent(err)
|
||||||
|
}
|
||||||
|
return ValidateAuthRequestClient(ctx, authReq, client, verifier)
|
||||||
|
}
|
||||||
if validater, ok := authorizer.(AuthorizeValidator); ok {
|
if validater, ok := authorizer.(AuthorizeValidator); ok {
|
||||||
validation = validater.ValidateAuthRequest
|
validation = validater.ValidateAuthRequest
|
||||||
}
|
}
|
||||||
|
@ -112,11 +120,6 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
||||||
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer)
|
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID())
|
|
||||||
if err != nil {
|
|
||||||
AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
RedirectToLogin(req.GetID(), client, w, r)
|
RedirectToLogin(req.GetID(), client, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,7 +155,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}
|
||||||
|
@ -166,7 +169,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 != "" {
|
||||||
|
@ -211,26 +214,37 @@ func CopyRequestObjectToAuthRequest(authReq *oidc.AuthRequest, requestObject *oi
|
||||||
authReq.RequestParam = ""
|
authReq.RequestParam = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed
|
// ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed.
|
||||||
|
//
|
||||||
|
// Deprecated: Use [ValidateAuthRequestClient] to prevent querying for the Client twice.
|
||||||
func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) {
|
func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier *IDTokenHintVerifier) (sub string, err error) {
|
||||||
ctx, span := tracer.Start(ctx, "ValidateAuthRequest")
|
ctx, span := tracer.Start(ctx, "ValidateAuthRequest")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
client, err := storage.GetClientByClientID(ctx, authReq.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return "", oidc.ErrInvalidRequestRedirectURI().WithDescription("unable to retrieve client by id").WithParent(err)
|
||||||
|
}
|
||||||
|
return ValidateAuthRequestClient(ctx, authReq, client, verifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAuthRequestClient validates the Auth request against the passed client.
|
||||||
|
// If id_token_hint is part of the request, the subject of the token is returned.
|
||||||
|
func ValidateAuthRequestClient(ctx context.Context, authReq *oidc.AuthRequest, client Client, verifier *IDTokenHintVerifier) (sub string, err error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "ValidateAuthRequestClient")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge)
|
authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
client, err := storage.GetClientByClientID(ctx, authReq.ClientID)
|
|
||||||
if err != nil {
|
|
||||||
return "", oidc.DefaultToServerError(err, "unable to retrieve client by id")
|
|
||||||
}
|
|
||||||
authReq.Scopes, err = ValidateAuthReqScopes(client, authReq.Scopes)
|
authReq.Scopes, err = ValidateAuthReqScopes(client, authReq.Scopes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if err := ValidateAuthReqResponseType(client, authReq.ResponseType); err != nil {
|
if err := ValidateAuthReqResponseType(client, authReq.ResponseType); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -250,44 +264,30 @@ func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error)
|
||||||
return maxAge, nil
|
return maxAge, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAuthReqScopes validates the passed scopes
|
// ValidateAuthReqScopes validates the passed scopes and deletes any unsupported scopes.
|
||||||
|
// An error is returned if scopes is empty.
|
||||||
func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) {
|
func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) {
|
||||||
if len(scopes) == 0 {
|
if len(scopes) == 0 {
|
||||||
return nil, oidc.ErrInvalidRequest().
|
return nil, oidc.ErrInvalidRequest().
|
||||||
WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " +
|
WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " +
|
||||||
"If you have any questions, you may contact the administrator of the application.")
|
"If you have any questions, you may contact the administrator of the application.")
|
||||||
}
|
}
|
||||||
openID := false
|
scopes = slices.DeleteFunc(scopes, func(scope string) bool {
|
||||||
for i := len(scopes) - 1; i >= 0; i-- {
|
return !(scope == oidc.ScopeOpenID ||
|
||||||
scope := scopes[i]
|
scope == oidc.ScopeProfile ||
|
||||||
if scope == oidc.ScopeOpenID {
|
|
||||||
openID = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !(scope == oidc.ScopeProfile ||
|
|
||||||
scope == oidc.ScopeEmail ||
|
scope == oidc.ScopeEmail ||
|
||||||
scope == oidc.ScopePhone ||
|
scope == oidc.ScopePhone ||
|
||||||
scope == oidc.ScopeAddress ||
|
scope == oidc.ScopeAddress ||
|
||||||
scope == oidc.ScopeOfflineAccess) &&
|
scope == oidc.ScopeOfflineAccess) &&
|
||||||
!client.IsScopeAllowed(scope) {
|
!client.IsScopeAllowed(scope)
|
||||||
scopes[i] = scopes[len(scopes)-1]
|
})
|
||||||
scopes[len(scopes)-1] = ""
|
|
||||||
scopes = scopes[:len(scopes)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !openID {
|
|
||||||
return nil, oidc.ErrInvalidScope().WithDescription("The scope openid is missing in your request. " +
|
|
||||||
"Please ensure the scope openid is added to the request. " +
|
|
||||||
"If you have any questions, you may contact the administrator of the application.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return scopes, nil
|
return scopes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores
|
// checkURIAgainstRedirects just checks aginst the valid redirect URIs and ignores
|
||||||
// other factors.
|
// other factors.
|
||||||
func checkURIAgainstRedirects(client Client, uri string) error {
|
func checkURIAgainstRedirects(client Client, uri string) error {
|
||||||
if str.Contains(client.RedirectURIs(), uri) {
|
if slices.Contains(client.RedirectURIs(), uri) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if globClient, ok := client.(HasRedirectGlobs); ok {
|
if globClient, ok := client.(HasRedirectGlobs); ok {
|
||||||
|
@ -312,12 +312,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
|
||||||
}
|
}
|
||||||
|
@ -338,12 +338,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
|
||||||
}
|
}
|
||||||
|
@ -373,11 +376,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
|
||||||
|
|
|
@ -137,11 +137,6 @@ func TestValidateAuthRequest(t *testing.T) {
|
||||||
args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil},
|
args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil},
|
||||||
oidc.ErrInvalidRequest(),
|
oidc.ErrInvalidRequest(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"scope openid missing fails",
|
|
||||||
args{&oidc.AuthRequest{Scopes: []string{"profile"}}, mock.NewMockStorageExpectValidClientID(t), nil},
|
|
||||||
oidc.ErrInvalidScope(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"response_type missing fails",
|
"response_type missing fails",
|
||||||
args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil},
|
args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil},
|
||||||
|
@ -287,16 +282,6 @@ func TestValidateAuthReqScopes(t *testing.T) {
|
||||||
err: true,
|
err: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"scope openid missing fails",
|
|
||||||
args{
|
|
||||||
mock.NewClientExpectAny(t, op.ApplicationTypeWeb),
|
|
||||||
[]string{"email"},
|
|
||||||
},
|
|
||||||
res{
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"scope ok",
|
"scope ok",
|
||||||
args{
|
args{
|
||||||
|
@ -448,6 +433,24 @@ func TestValidateAuthReqRedirectURI(t *testing.T) {
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"code flow registered https loopback v4 native ok",
|
||||||
|
args{
|
||||||
|
"https://127.0.0.1:4200/callback",
|
||||||
|
mock.NewClientWithConfig(t, []string{"https://127.0.0.1/callback"}, op.ApplicationTypeNative, nil, false),
|
||||||
|
oidc.ResponseTypeCode,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code flow registered https loopback v6 native ok",
|
||||||
|
args{
|
||||||
|
"https://[::1]:4200/callback",
|
||||||
|
mock.NewClientWithConfig(t, []string{"https://[::1]/callback"}, op.ApplicationTypeNative, nil, false),
|
||||||
|
oidc.ResponseTypeCode,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"code flow unregistered http native fails",
|
"code flow unregistered http native fails",
|
||||||
args{
|
args{
|
||||||
|
|
|
@ -49,6 +49,9 @@ type Configuration interface {
|
||||||
|
|
||||||
SupportedUILocales() []language.Tag
|
SupportedUILocales() []language.Tag
|
||||||
DeviceAuthorization() DeviceAuthorizationConfig
|
DeviceAuthorization() DeviceAuthorizationConfig
|
||||||
|
|
||||||
|
BackChannelLogoutSupported() bool
|
||||||
|
BackChannelLogoutSessionSupported() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type IssuerFromRequest func(r *http.Request) string
|
type IssuerFromRequest func(r *http.Request) string
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -61,6 +61,8 @@ func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage Di
|
||||||
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
|
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
|
||||||
UILocalesSupported: config.SupportedUILocales(),
|
UILocalesSupported: config.SupportedUILocales(),
|
||||||
RequestParameterSupported: config.RequestObjectSupported(),
|
RequestParameterSupported: config.RequestObjectSupported(),
|
||||||
|
BackChannelLogoutSupported: config.BackChannelLogoutSupported(),
|
||||||
|
BackChannelLogoutSessionSupported: config.BackChannelLogoutSessionSupported(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,11 +94,17 @@ func createDiscoveryConfigV2(ctx context.Context, config Configuration, storage
|
||||||
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
|
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
|
||||||
UILocalesSupported: config.SupportedUILocales(),
|
UILocalesSupported: config.SupportedUILocales(),
|
||||||
RequestParameterSupported: config.RequestObjectSupported(),
|
RequestParameterSupported: config.RequestObjectSupported(),
|
||||||
|
BackChannelLogoutSupported: config.BackChannelLogoutSupported(),
|
||||||
|
BackChannelLogoutSessionSupported: config.BackChannelLogoutSessionSupported(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Scopes(c Configuration) []string {
|
func Scopes(c Configuration) []string {
|
||||||
return DefaultSupportedScopes // TODO: config
|
provider, ok := c.(*Provider)
|
||||||
|
if ok && provider.config.SupportedScopes != nil {
|
||||||
|
return provider.config.SupportedScopes
|
||||||
|
}
|
||||||
|
return DefaultSupportedScopes
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResponseTypes(c Configuration) []string {
|
func ResponseTypes(c Configuration) []string {
|
||||||
|
@ -131,7 +139,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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -428,6 +428,7 @@ func TestTryErrorRedirect(t *testing.T) {
|
||||||
parent: oidc.ErrInteractionRequired().WithDescription("sign in"),
|
parent: oidc.ErrInteractionRequired().WithDescription("sign in"),
|
||||||
},
|
},
|
||||||
want: &Redirect{
|
want: &Redirect{
|
||||||
|
Header: make(http.Header),
|
||||||
URL: "http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1",
|
URL: "http://example.com/callback?error=interaction_required&error_description=sign+in&state=state1",
|
||||||
},
|
},
|
||||||
wantLog: `{
|
wantLog: `{
|
||||||
|
|
|
@ -78,6 +78,34 @@ func (mr *MockConfigurationMockRecorder) AuthorizationEndpoint() *gomock.Call {
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).AuthorizationEndpoint))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).AuthorizationEndpoint))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackChannelLogoutSessionSupported mocks base method.
|
||||||
|
func (m *MockConfiguration) BackChannelLogoutSessionSupported() bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "BackChannelLogoutSessionSupported")
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackChannelLogoutSessionSupported indicates an expected call of BackChannelLogoutSessionSupported.
|
||||||
|
func (mr *MockConfigurationMockRecorder) BackChannelLogoutSessionSupported() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackChannelLogoutSessionSupported", reflect.TypeOf((*MockConfiguration)(nil).BackChannelLogoutSessionSupported))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackChannelLogoutSupported mocks base method.
|
||||||
|
func (m *MockConfiguration) BackChannelLogoutSupported() bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "BackChannelLogoutSupported")
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackChannelLogoutSupported indicates an expected call of BackChannelLogoutSupported.
|
||||||
|
func (mr *MockConfigurationMockRecorder) BackChannelLogoutSupported() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackChannelLogoutSupported", reflect.TypeOf((*MockConfiguration)(nil).BackChannelLogoutSupported))
|
||||||
|
}
|
||||||
|
|
||||||
// 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()
|
||||||
|
|
11
pkg/op/op.go
11
pkg/op/op.go
|
@ -167,7 +167,10 @@ 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
|
||||||
|
BackChannelLogoutSessionSupported bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoints defines endpoint routes.
|
// Endpoints defines endpoint routes.
|
||||||
|
@ -411,6 +414,14 @@ func (o *Provider) DeviceAuthorization() DeviceAuthorizationConfig {
|
||||||
return o.config.DeviceAuthorization
|
return o.config.DeviceAuthorization
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Provider) BackChannelLogoutSupported() bool {
|
||||||
|
return o.config.BackChannelLogoutSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Provider) BackChannelLogoutSessionSupported() bool {
|
||||||
|
return o.config.BackChannelLogoutSessionSupported
|
||||||
|
}
|
||||||
|
|
||||||
func (o *Provider) Storage() Storage {
|
func (o *Provider) Storage() Storage {
|
||||||
return o.storage
|
return o.storage
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -218,6 +218,7 @@ type Response struct {
|
||||||
// without custom headers.
|
// without custom headers.
|
||||||
func NewResponse(data any) *Response {
|
func NewResponse(data any) *Response {
|
||||||
return &Response{
|
return &Response{
|
||||||
|
Header: make(http.Header),
|
||||||
Data: data,
|
Data: data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -242,11 +243,14 @@ type Redirect struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRedirect(url string) *Redirect {
|
func NewRedirect(url string) *Redirect {
|
||||||
return &Redirect{URL: url}
|
return &Redirect{
|
||||||
|
Header: make(http.Header),
|
||||||
|
URL: url,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (red *Redirect) writeOut(w http.ResponseWriter, r *http.Request) {
|
func (red *Redirect) writeOut(w http.ResponseWriter, r *http.Request) {
|
||||||
gu.MapMerge(r.Header, w.Header())
|
gu.MapMerge(red.Header, w.Header())
|
||||||
http.Redirect(w, r, red.URL, http.StatusFound)
|
http.Redirect(w, r, red.URL, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue