Merge remote-tracking branch 'origin/back-channel-support' into back-channel-support

This commit is contained in:
Livio Spring 2024-10-29 08:28:22 +01:00
commit b84a56c377
No known key found for this signature in database
GPG key ID: 26BB1C2FA5952CF0
10 changed files with 273 additions and 32 deletions

View file

@ -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.5.0 - uses: codecov/codecov-action@v4.6.0
with: with:
file: ./profile.cov file: ./profile.cov
name: codecov-go name: codecov-go

View file

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

View file

@ -0,0 +1,40 @@
package config
import (
"os"
"strings"
)
const (
// default port for the http server to run
DefaultIssuerPort = "9998"
)
type Config struct {
Port string
RedirectURI []string
UsersFile string
}
// FromEnvVars loads configuration parameters from environment variables.
// If there is no such variable defined, then use default values.
func FromEnvVars(defaults *Config) *Config {
if defaults == nil {
defaults = &Config{}
}
cfg := &Config{
Port: defaults.Port,
RedirectURI: defaults.RedirectURI,
UsersFile: defaults.UsersFile,
}
if value, ok := os.LookupEnv("PORT"); ok {
cfg.Port = value
}
if value, ok := os.LookupEnv("USERS_FILE"); ok {
cfg.UsersFile = value
}
if value, ok := os.LookupEnv("REDIRECT_URI"); ok {
cfg.RedirectURI = strings.Split(value, ",")
}
return cfg
}

View file

@ -0,0 +1,77 @@
package config
import (
"fmt"
"os"
"testing"
)
func TestFromEnvVars(t *testing.T) {
for _, tc := range []struct {
name string
env map[string]string
defaults *Config
want *Config
}{
{
name: "no vars, no default values",
env: map[string]string{},
want: &Config{},
},
{
name: "no vars, only defaults",
env: map[string]string{},
defaults: &Config{
Port: "6666",
UsersFile: "/default/user/path",
RedirectURI: []string{"re", "direct", "uris"},
},
want: &Config{
Port: "6666",
UsersFile: "/default/user/path",
RedirectURI: []string{"re", "direct", "uris"},
},
},
{
name: "overriding default values",
env: map[string]string{
"PORT": "1234",
"USERS_FILE": "/path/to/users",
"REDIRECT_URI": "http://redirect/redirect",
},
defaults: &Config{
Port: "6666",
UsersFile: "/default/user/path",
RedirectURI: []string{"re", "direct", "uris"},
},
want: &Config{
Port: "1234",
UsersFile: "/path/to/users",
RedirectURI: []string{"http://redirect/redirect"},
},
},
{
name: "multiple redirect uris",
env: map[string]string{
"REDIRECT_URI": "http://host_1,http://host_2,http://host_3",
},
want: &Config{
RedirectURI: []string{
"http://host_1", "http://host_2", "http://host_3",
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
os.Clearenv()
for k, v := range tc.env {
os.Setenv(k, v)
}
cfg := FromEnvVars(tc.defaults)
if fmt.Sprint(cfg) != fmt.Sprint(tc.want) {
t.Errorf("Expected FromEnvVars()=%q, but got %q", tc.want, cfg)
}
})
}
}

View file

@ -5,20 +5,33 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"strings"
"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" logger := slog.New(
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}),
)
//which gives us the issuer: http://localhost:9998/ //which gives us the issuer: http://localhost:9998/
issuer := fmt.Sprintf("http://localhost:%s/", port) issuer := fmt.Sprintf("http://localhost:%s/", cfg.Port)
storage.RegisterClients( storage.RegisterClients(
storage.NativeClient("native", strings.Split(os.Getenv("REDIRECT_URI"), ",")...), storage.NativeClient("native", cfg.RedirectURI...),
storage.WebClient("web", "secret"), storage.WebClient("web", "secret"),
storage.WebClient("api", "secret"), storage.WebClient("api", "secret"),
) )
@ -26,23 +39,20 @@ func main() {
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations // the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
// this might be the layer for accessing your database // this might be the layer for accessing your database
// in this example it will be handled in-memory // in this example it will be handled in-memory
storage := storage.NewStorage(storage.NewUserStore(issuer)) store, err := getUserStore(cfg)
if err != nil {
logger := slog.New( logger.Error("cannot create UserStore", "error", err)
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ os.Exit(1)
AddSource: true, }
Level: slog.LevelDebug, storage := storage.NewStorage(store)
}),
)
router := exampleop.SetupServer(issuer, storage, logger, false) router := exampleop.SetupServer(issuer, storage, logger, false)
server := &http.Server{ server := &http.Server{
Addr: ":" + port, Addr: ":" + cfg.Port,
Handler: router, Handler: router,
} }
logger.Info("server listening, press ctrl+c to stop", "addr", fmt.Sprintf("http://localhost:%s/", port)) logger.Info("server listening, press ctrl+c to stop", "addr", issuer)
err := server.ListenAndServe() if server.ListenAndServe() != http.ErrServerClosed {
if err != http.ErrServerClosed {
logger.Error("server terminated", "error", err) logger.Error("server terminated", "error", err)
os.Exit(1) os.Exit(1)
} }

View file

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

View file

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

View file

@ -0,0 +1,70 @@
package storage
import (
"os"
"path"
"reflect"
"testing"
"golang.org/x/text/language"
)
func TestStoreFromFile(t *testing.T) {
for _, tc := range []struct {
name string
pathToFile string
content string
want UserStore
wantErr bool
}{
{
name: "normal user file",
pathToFile: "userfile.json",
content: `{
"id1": {
"ID": "id1",
"EmailVerified": true,
"PreferredLanguage": "DE"
}
}`,
want: userStore{map[string]*User{
"id1": {
ID: "id1",
EmailVerified: true,
PreferredLanguage: language.German,
},
}},
},
{
name: "malformed file",
pathToFile: "whatever",
content: "not a json just a text",
wantErr: true,
},
{
name: "not existing file",
pathToFile: "what/ever/file",
wantErr: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
actualPath := path.Join(t.TempDir(), tc.pathToFile)
if tc.content != "" && tc.pathToFile != "" {
if err := os.WriteFile(actualPath, []byte(tc.content), 0666); err != nil {
t.Fatalf("cannot create file with test content: %q", tc.content)
}
}
result, err := StoreFromFile(actualPath)
if err != nil && !tc.wantErr {
t.Errorf("StoreFromFile(%q) returned unexpected error %q", tc.pathToFile, err)
} else if err == nil && tc.wantErr {
t.Errorf("StoreFromFile(%q) did not return an expected error", tc.pathToFile)
}
if !tc.wantErr && !reflect.DeepEqual(tc.want, result.(userStore)) {
t.Errorf("expected StoreFromFile(%q) = %v, but got %v",
tc.pathToFile, tc.want, result)
}
})
}
}

6
go.mod
View file

@ -3,7 +3,7 @@ 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.7.1
github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/chi/v5 v5.1.0
github.com/go-jose/go-jose/v4 v4.0.4 github.com/go-jose/go-jose/v4 v4.0.4
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
@ -16,11 +16,11 @@ require (
github.com/rs/cors v1.11.1 github.com/rs/cors v1.11.1
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.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.29.0 go.opentelemetry.io/otel v1.29.0
golang.org/x/oauth2 v0.23.0 golang.org/x/oauth2 v0.23.0
golang.org/x/text v0.18.0 golang.org/x/text v0.19.0
) )
require ( require (

12
go.sum
View file

@ -1,5 +1,5 @@
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.7.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=
@ -50,8 +50,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.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.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
@ -88,8 +88,8 @@ golang.org/x/sys v0.22.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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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=