feat(example): Allow configuring some parameters with env variables (#663)
Co-authored-by: Andrey Rusakov <andrey.rusakov@camptocamp.com>
This commit is contained in:
parent
9f7cbb0dbf
commit
24869d2811
6 changed files with 262 additions and 21 deletions
38
README.md
38
README.md
|
@ -23,7 +23,7 @@ Whenever possible we tried to reuse / extend existing packages like `OAuth2 for
|
|||
The most important packages of the library:
|
||||
<pre>
|
||||
/pkg
|
||||
/client clients using the OP for retrieving, exchanging and verifying tokens
|
||||
/client clients using the OP for retrieving, exchanging and verifying tokens
|
||||
/rp definition and implementation of an OIDC Relying Party (client)
|
||||
/rs definition and implementation of an OAuth Resource Server (API)
|
||||
/op definition and implementation of an OIDC OpenID Provider (server)
|
||||
|
@ -55,14 +55,14 @@ CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid
|
|||
```
|
||||
|
||||
- open http://localhost:9999/login in your browser
|
||||
- you will be redirected to op server and the login UI
|
||||
- you will be redirected to op server and the login UI
|
||||
- login with user `test-user@localhost` and password `verysecure`
|
||||
- the OP will redirect you to the client app, which displays the user info
|
||||
|
||||
for the dynamic issuer, just start it with:
|
||||
```bash
|
||||
go run github.com/zitadel/oidc/v3/example/server/dynamic
|
||||
```
|
||||
```
|
||||
the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with:
|
||||
```bash
|
||||
CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v3/example/client/app
|
||||
|
@ -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`)
|
||||
|
||||
### 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
|
||||
|
||||
| | Relying party | OpenID Provider | Specification |
|
||||
|
@ -115,7 +145,7 @@ For your convenience you can find the relevant guides linked below.
|
|||
|
||||
## Supported Go Versions
|
||||
|
||||
For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:).
|
||||
For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:).
|
||||
Versions that also build are marked with :warning:.
|
||||
|
||||
| Version | Supported |
|
||||
|
|
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -5,20 +5,33 @@ import (
|
|||
"log/slog"
|
||||
"net/http"
|
||||
"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/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() {
|
||||
//we will run on :9998
|
||||
port := "9998"
|
||||
cfg := config.FromEnvVars(&config.Config{Port: "9998"})
|
||||
logger := slog.New(
|
||||
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: slog.LevelDebug,
|
||||
}),
|
||||
)
|
||||
|
||||
//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.NativeClient("native", strings.Split(os.Getenv("REDIRECT_URI"), ",")...),
|
||||
storage.NativeClient("native", cfg.RedirectURI...),
|
||||
storage.WebClient("web", "secret"),
|
||||
storage.WebClient("api", "secret"),
|
||||
)
|
||||
|
@ -26,23 +39,20 @@ func main() {
|
|||
// 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(
|
||||
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: slog.LevelDebug,
|
||||
}),
|
||||
)
|
||||
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)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Addr: ":" + cfg.Port,
|
||||
Handler: router,
|
||||
}
|
||||
logger.Info("server listening, press ctrl+c to stop", "addr", fmt.Sprintf("http://localhost:%s/", port))
|
||||
err := server.ListenAndServe()
|
||||
if err != http.ErrServerClosed {
|
||||
logger.Info("server listening, press ctrl+c to stop", "addr", issuer)
|
||||
if server.ListenAndServe() != http.ErrServerClosed {
|
||||
logger.Error("server terminated", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package storage
|
|||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
@ -35,6 +37,18 @@ type userStore struct {
|
|||
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 {
|
||||
hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue