Skip to content

Commit

Permalink
feat(example): Allow configuring some parameters with env variables
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrey Rusakov committed Oct 9, 2024
1 parent 5ae555e commit 0410628
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 21 deletions.
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -55,21 +55,51 @@ 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
```

> 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_URIS | 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 |
Expand Down Expand Up @@ -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 |
Expand Down
40 changes: 40 additions & 0 deletions example/server/config/config.go
Original file line number Diff line number Diff line change
@@ -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
RedirectURIs []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,
RedirectURIs: defaults.RedirectURIs,
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_URIS"); ok {
cfg.RedirectURIs = strings.Split(value, ",")
}
return cfg
}
77 changes: 77 additions & 0 deletions example/server/config/config_test.go
Original file line number Diff line number Diff line change
@@ -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",
RedirectURIs: []string{"re", "direct", "uris"},
},
want: &Config{
Port: "6666",
UsersFile: "/default/user/path",
RedirectURIs: []string{"re", "direct", "uris"},
},
},
{
name: "overriding default values",
env: map[string]string{
"PORT": "1234",
"USERS_FILE": "/path/to/users",
"REDIRECT_URIS": "http://redirect/redirect",
},
defaults: &Config{
Port: "6666",
UsersFile: "/default/user/path",
RedirectURIs: []string{"re", "direct", "uris"},
},
want: &Config{
Port: "1234",
UsersFile: "/path/to/users",
RedirectURIs: []string{"http://redirect/redirect"},
},
},
{
name: "multiple redirect uris",
env: map[string]string{
"REDIRECT_URIS": "http://host_1,http://host_2,http://host_3",
},
want: &Config{
RedirectURIs: []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)
}
})
}
}
44 changes: 27 additions & 17 deletions example/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,54 @@ 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.RedirectURIs...),
storage.WebClient("web", "secret"),
storage.WebClient("api", "secret"),
)

// 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)
}
Expand Down
14 changes: 14 additions & 0 deletions example/server/storage/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package storage

import (
"crypto/rsa"
"encoding/json"
"os"
"strings"

"golang.org/x/text/language"
Expand Down Expand Up @@ -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{
Expand Down
70 changes: 70 additions & 0 deletions example/server/storage/user_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit 0410628

Please sign in to comment.