Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4ab31f0

Browse files
authoredApr 5, 2025
feat: multiple clients (#3)
1 parent 46c4663 commit 4ab31f0

File tree

21 files changed

+469
-199
lines changed

21 files changed

+469
-199
lines changed
 

‎config.go

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package main
33
import (
44
"errors"
55
"fmt"
6+
"net/url"
67
"os"
7-
"path"
88
"path/filepath"
99

1010
"github.com/spf13/cobra"
1111

12+
"authelia.com/tools/ac/config"
1213
"authelia.com/tools/ac/consts"
14+
"authelia.com/tools/ac/utilities"
1315
)
1416

1517
func newConfigCmd(ctx *cmdctx) (cmd *cobra.Command) {
@@ -31,6 +33,8 @@ func newConfigGenerateCmd(ctx *cmdctx) (cmd *cobra.Command) {
3133
RunE: ctx.handleConfigGenerateRunE,
3234
}
3335

36+
cmd.Flags().String("name", "example", "Name for the example client")
37+
3438
return cmd
3539
}
3640

@@ -46,29 +50,68 @@ func (ctx *cmdctx) handleConfigGenerateRunE(cmd *cobra.Command, args []string) (
4650
}
4751
}
4852

53+
name, err := cmd.Flags().GetString("name")
54+
if err != nil {
55+
return err
56+
}
57+
58+
rawAutheliaURL, err := cmd.Flags().GetString("authelia-url")
59+
if err != nil {
60+
return err
61+
}
62+
63+
if len(rawAutheliaURL) == 0 {
64+
rawAutheliaURL = "https://auth.example.com"
65+
}
66+
67+
autheliaURL, err := url.Parse(rawAutheliaURL)
68+
if err != nil {
69+
return err
70+
}
71+
72+
id, err := utilities.GetRandomBytes(80, utilities.CharsetAlphaNumeric)
73+
if err != nil {
74+
return fmt.Errorf("error generating config: error generating client id: %w", err)
75+
}
76+
77+
secret, err := utilities.GetRandomBytes(100, utilities.CharsetAlphaNumeric)
78+
if err != nil {
79+
return fmt.Errorf("error generating config: error generating client secret: %w", err)
80+
}
81+
82+
c := &config.Configuration{
83+
AutheliaURL: config.NewURL(autheliaURL),
84+
Storage: "storage.yml",
85+
OAuth2: config.OAuth2{
86+
DefaultClient: name,
87+
Clients: map[string]config.OAuth2Client{
88+
name: {
89+
ID: string(id),
90+
Secret: string(secret),
91+
PAR: true,
92+
Scope: []string{"offline_access", "authelia.bearer.authz"},
93+
Audience: []string{"https://app.example.com"},
94+
GrantType: "authorization_code",
95+
TokenEndpointAuthMethod: "client_secret_post",
96+
OfflineAccess: true,
97+
},
98+
},
99+
},
100+
Server: config.Server{
101+
Port: 9091,
102+
},
103+
}
104+
49105
var (
50106
f *os.File
51107
data []byte
52108
)
53109

54-
switch ext := filepath.Ext(pathOut); ext {
55-
case ".json":
56-
if data, err = internalFS.ReadFile(path.Join("embed", "config", "config.json")); err != nil {
57-
return err
58-
}
59-
case ".yml", ".yaml":
60-
if data, err = internalFS.ReadFile(path.Join("embed", "config", "config.yaml")); err != nil {
61-
return err
62-
}
63-
case ".tml", ".toml":
64-
if data, err = internalFS.ReadFile(path.Join("embed", "config", "config.toml")); err != nil {
65-
return err
66-
}
67-
default:
68-
return fmt.Errorf("error generating config: extension '%s' for file '%s' is not supported", ext, pathOut)
110+
if data, err = utilities.AutoMarshal(c, filepath.Ext(pathOut)); err != nil {
111+
return fmt.Errorf("error generating config: %w", err)
69112
}
70113

71-
if f, err = os.OpenFile(pathOut, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0640); err != nil {
114+
if f, err = os.OpenFile(pathOut, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0640); err != nil {
72115
return err
73116
}
74117

@@ -78,6 +121,8 @@ func (ctx *cmdctx) handleConfigGenerateRunE(cmd *cobra.Command, args []string) (
78121
return err
79122
}
80123

124+
_, _ = fmt.Fprintf(os.Stdout, "\nGenerated config: %s\n", pathOut)
125+
81126
return nil
82127
}
83128

‎config/config.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import (
1111
"github.com/knadh/koanf/providers/posflag"
1212
"github.com/knadh/koanf/v2"
1313
"github.com/spf13/pflag"
14+
15+
"authelia.com/tools/ac/consts"
1416
)
1517

18+
// Load the configuration.
1619
func Load(paths []string, flags *pflag.FlagSet, flagsPrefix string) (config *Configuration, err error) {
1720
ko := koanf.New(".")
1821

@@ -26,11 +29,11 @@ func Load(paths []string, flags *pflag.FlagSet, flagsPrefix string) (config *Con
2629
provider := file.Provider(path)
2730

2831
switch ext := filepath.Ext(path); ext {
29-
case ".yaml", ".yml", ".json":
32+
case consts.FileTypeYAML, consts.FileTypeAltYAML, consts.FileTypeJSON:
3033
if err = ko.Load(provider, yaml.Parser()); err != nil {
3134
return nil, fmt.Errorf("error occurred loading file configuration: %w", err)
3235
}
33-
case ".tml", ".toml":
36+
case consts.FileTypeTOML, consts.FileTypeAltTOML:
3437
if err = ko.Load(provider, toml.Parser()); err != nil {
3538
return nil, fmt.Errorf("error occurred loading file configuration: %w", err)
3639
}

‎config/const.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package config
22

33
const (
4-
errFmtRequiredOptionOAuth2Bearer = "error validating configuration: oauth2: bearer: the '%s' value is required"
5-
errFmtNotKnownOptionOAuth2Bearer = "error validating configuration: oauth2: bearer: the '%s' value of '%s' is not known"
4+
errFmtRequiredOptionOAuth2Bearer = "the '%s' value is required"
5+
errFmtNotKnownOptionOAuth2Bearer = "the '%s' value of '%s' is not known"
66
)

‎config/hook.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func mStringToURLHookFunc() mapstructure.DecodeHookFuncType {
2525
prefixType = "*"
2626
}
2727

28-
expectedType := reflect.TypeOf(url.URL{})
28+
expectedType := reflect.TypeOf(URL{})
2929

3030
if ptr && t.Elem() != expectedType {
3131
return data, nil
@@ -44,14 +44,14 @@ func mStringToURLHookFunc() mapstructure.DecodeHookFuncType {
4444
}
4545

4646
if ptr {
47-
return result, nil
47+
return (*URL)(result), nil
4848
}
4949

5050
if result == nil {
51-
return url.URL{}, nil
51+
return URL{}, nil
5252
}
5353

54-
return *result, nil
54+
return (URL)(*result), nil
5555
}
5656
}
5757

‎config/save.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"authelia.com/tools/ac/utilities"
9+
)
10+
11+
func (config *Configuration) Save(path string) (err error) {
12+
var data []byte
13+
14+
if data, err = utilities.AutoMarshal(config, filepath.Ext(path)); err != nil {
15+
return fmt.Errorf("error occurred marshalling the updated storage: %w", err)
16+
}
17+
18+
var f *os.File
19+
20+
if f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0640); err != nil {
21+
return err
22+
}
23+
24+
defer f.Close()
25+
26+
if _, err = f.Write(data); err != nil {
27+
return err
28+
}
29+
30+
return nil
31+
}

‎config/schema.go

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,42 @@ import (
55
)
66

77
type Configuration struct {
8-
AutheliaURL *url.URL `koanf:"authelia_url"`
9-
Storage string `koanf:"storage"`
10-
OAuth2 OAuth2 `koanf:"oauth2"`
11-
Server Server `koanf:"server"`
8+
AutheliaURL *URL `koanf:"authelia_url" json:"authelia_url,omitempty" yaml:"authelia_url,omitempty" toml:"authelia_url,omitempty"`
9+
Storage string `koanf:"storage" json:"storage,omitempty" yaml:"storage,omitempty" toml:"storage,omitempty"`
10+
OAuth2 OAuth2 `koanf:"oauth2" json:"oauth2" yaml:"oauth2" toml:"oauth2"`
11+
Server Server `koanf:"server" json:"server" yaml:"server" toml:"server"`
1212
}
1313

1414
type Server struct {
15-
Port uint16 `koanf:"port"`
15+
Port uint16 `koanf:"port" json:"port" yaml:"port" toml:"port"`
1616
}
1717

1818
type OAuth2 struct {
19-
Bearer OAuth2Bearer `koanf:"bearer"`
19+
DefaultClient string `koanf:"default_client" json:"default_client,omitempty" yaml:"default_client,omitempty" toml:"default_client,omitempty"`
20+
Clients OAuth2Clients `koanf:"clients" json:"clients" yaml:"clients" toml:"clients"`
2021
}
2122

22-
type OAuth2Bearer struct {
23-
ID string `koanf:"id"`
24-
Secret string `koanf:"secret"`
25-
PAR bool `koanf:"par"`
26-
Audience []string `koanf:"audience"`
27-
Scope []string `koanf:"scope"`
28-
GrantType string `koanf:"grant_type"`
29-
TokenEndpointAuthMethod string `koanf:"token_endpoint_auth_method"`
30-
OfflineAccess bool `koanf:"offline_access"`
23+
type OAuth2Clients map[string]OAuth2Client
24+
25+
type OAuth2Client struct {
26+
ID string `koanf:"id" json:"id" yaml:"id" toml:"id"`
27+
Secret string `koanf:"secret" json:"secret" yaml:"secret" toml:"secret"`
28+
PAR bool `koanf:"par" json:"par" yaml:"par" toml:"par"`
29+
Audience []string `koanf:"audience" json:"audience" yaml:"audience" toml:"audience"`
30+
Scope []string `koanf:"scope" json:"scope" yaml:"scope" toml:"scope"`
31+
GrantType string `koanf:"grant_type" json:"grant_type" yaml:"grant_type" toml:"grant_type"`
32+
TokenEndpointAuthMethod string `koanf:"token_endpoint_auth_method" json:"token_endpoint_auth_method" yaml:"token_endpoint_auth_method" toml:"token_endpoint_auth_method"`
33+
OfflineAccess bool `koanf:"offline_access" json:"offline_access" yaml:"offline_access" toml:"offline_access"`
34+
}
35+
36+
func NewURL(in *url.URL) *URL {
37+
return (*URL)(in)
38+
}
39+
40+
type URL url.URL
41+
42+
func (u *URL) MarshalText() (text []byte, err error) {
43+
v := (*url.URL)(u)
44+
45+
return []byte(v.String()), nil
3146
}

‎config/validate.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,31 @@ func (config *Configuration) Validate() (err error) {
1818
return nil
1919
}
2020

21-
func (config *OAuth2Bearer) Validate() (err error) {
21+
func (config *OAuth2) Validate() (err error) {
22+
if err = config.Clients.Validate(); err != nil {
23+
return err
24+
}
25+
26+
if config.DefaultClient != "" {
27+
if _, ok := config.Clients[config.DefaultClient]; !ok {
28+
return fmt.Errorf("error validating configuration: the default client '%s' does not exist", config.DefaultClient)
29+
}
30+
}
31+
32+
return nil
33+
}
34+
35+
func (config OAuth2Clients) Validate() (err error) {
36+
for name, client := range config {
37+
if err = client.Validate(); err != nil {
38+
return fmt.Errorf("error validating configuration: oauth2: clients: %s: %v", name, err)
39+
}
40+
}
41+
42+
return nil
43+
}
44+
45+
func (config *OAuth2Client) Validate() (err error) {
2246
if len(config.ID) == 0 {
2347
return fmt.Errorf(errFmtRequiredOptionOAuth2Bearer, consts.ID)
2448
}

‎consts/const.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,13 @@ const (
2020
ClientSecretPost = "client_secret_post"
2121
ClientSecretBasic = "client_secret_basic"
2222

23-
OAuth2Bearer = "oauth2.bearer"
23+
OAuth2 = "oauth2"
24+
)
25+
26+
const (
27+
FileTypeYAML = ".yaml"
28+
FileTypeJSON = ".json"
29+
FileTypeTOML = ".toml"
30+
FileTypeAltYAML = ".yml"
31+
FileTypeAltTOML = ".tml"
2432
)

‎context.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package main
22

33
import (
4-
"authelia.com/tools/ac/store"
54
"context"
65

76
"github.com/spf13/cobra"
87

98
"authelia.com/tools/ac/config"
109
"authelia.com/tools/ac/consts"
10+
"authelia.com/tools/ac/store"
1111
)
1212

1313
type cmdctx struct {
@@ -36,8 +36,8 @@ func (ctx *cmdctx) handleLoadConfigPreRunE(prefix string) func(cmd *cobra.Comman
3636
}
3737

3838
switch prefix {
39-
case consts.OAuth2Bearer:
40-
if err = ctx.config.OAuth2.Bearer.Validate(); err != nil {
39+
case consts.OAuth2:
40+
if err = ctx.config.OAuth2.Validate(); err != nil {
4141
return err
4242
}
4343
}

‎embed.go

Lines changed: 0 additions & 6 deletions
This file was deleted.

‎embed/config/config.json

Lines changed: 0 additions & 24 deletions
This file was deleted.

‎embed/config/config.toml

Lines changed: 0 additions & 15 deletions
This file was deleted.

‎embed/config/config.yaml

Lines changed: 0 additions & 17 deletions
This file was deleted.

‎go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
module authelia.com/tools/ac
22

3-
go 1.23.0
3+
go 1.24.0
44

55
toolchain go1.24.1
66

77
require (
8-
authelia.com/client/oauth2 v0.0.0-20250405005328-fd752214e408
8+
authelia.com/client/oauth2 v0.0.0-20250405043315-6378a9b2a190
99
github.com/go-viper/mapstructure/v2 v2.2.1
1010
github.com/knadh/koanf/parsers/toml v0.1.0
1111
github.com/knadh/koanf/parsers/yaml v0.1.0
1212
github.com/knadh/koanf/providers/confmap v0.1.0
1313
github.com/knadh/koanf/providers/file v1.1.2
1414
github.com/knadh/koanf/providers/posflag v0.1.0
1515
github.com/knadh/koanf/v2 v2.1.2
16+
github.com/pelletier/go-toml v1.9.5
1617
github.com/spf13/cobra v1.9.1
1718
github.com/spf13/pflag v1.0.6
1819
gopkg.in/yaml.v3 v3.0.1
@@ -24,6 +25,5 @@ require (
2425
github.com/knadh/koanf/maps v0.1.2 // indirect
2526
github.com/mitchellh/copystructure v1.2.0 // indirect
2627
github.com/mitchellh/reflectwalk v1.0.2 // indirect
27-
github.com/pelletier/go-toml v1.9.5 // indirect
2828
golang.org/x/sys v0.31.0 // indirect
2929
)

‎go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
authelia.com/client/oauth2 v0.0.0-20250405005328-fd752214e408 h1:BPZsJzlRlgK7MakzWBRZGuZg6Y9NQZu0vzrSZf9fjNc=
2-
authelia.com/client/oauth2 v0.0.0-20250405005328-fd752214e408/go.mod h1:f0e/AQgp3qHJ2gSVnCheQZ4gTCm7BHasGpWrce36n9Q=
1+
authelia.com/client/oauth2 v0.0.0-20250405043315-6378a9b2a190 h1:5YfShMnyeIOFX5C1I7i6YrEpIfQCeeDBFTjau/iLfVU=
2+
authelia.com/client/oauth2 v0.0.0-20250405043315-6378a9b2a190/go.mod h1:f0e/AQgp3qHJ2gSVnCheQZ4gTCm7BHasGpWrce36n9Q=
33
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
44
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
55
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

‎oauth2.go

Lines changed: 241 additions & 65 deletions
Large diffs are not rendered by default.

‎store/save.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,13 @@ import (
55
"os"
66
"path/filepath"
77

8-
"github.com/pelletier/go-toml"
9-
"gopkg.in/yaml.v3"
8+
"authelia.com/tools/ac/utilities"
109
)
1110

1211
func (s *Storage) Save(path string) (err error) {
1312
var data []byte
1413

15-
switch ext := filepath.Ext(path); ext {
16-
case ".yml", ".yaml":
17-
data, err = yaml.Marshal(s)
18-
case ".tml", ".toml":
19-
data, err = toml.Marshal(s)
20-
default:
21-
err = fmt.Errorf("extension '%s' for file '%s' is not supported", ext, path)
22-
}
23-
24-
if err != nil {
14+
if data, err = utilities.AutoMarshal(s, filepath.Ext(path)); err != nil {
2515
return fmt.Errorf("error occurred marshalling the updated storage: %w", err)
2616
}
2717

‎store/schema.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import (
55
)
66

77
type Storage struct {
8-
Tokens map[string]Token
8+
// Tokens is a list of relevant tokens for a given client configuration. The key is the client name.
9+
Tokens map[string]Token `koanf:"tokens" json:"tokens,omitempty" yaml:"tokens,omitempty" toml:"tokens,omitempty"`
910
}
1011

1112
type Token struct {
12-
AccessToken string `koanf:"access_token" yaml:"access_token" json:"access_token"`
13-
RefreshToken string `koanf:"refresh_token" yaml:"refresh_token,omitempty" json:"refresh_token,omitempty"`
14-
IDToken string `koanf:"id_token" yaml:"id_token,omitempty" json:"id_token,omitempty"`
15-
TokenType string `koanf:"token_type" yaml:"token_type,omitempty" json:"token_type,omitempty"`
16-
Scope string `koanf:"scope" yaml:"scope,omitempty" json:"scope,omitempty"`
17-
Expiry time.Time `koanf:"expiry" yaml:"expiry,omitempty" json:"expiry,omitempty"`
13+
AccessToken string `koanf:"access_token" json:"access_token" yaml:"access_token" toml:"access_token"`
14+
RefreshToken string `koanf:"refresh_token" json:"refresh_token,omitempty" yaml:"refresh_token,omitempty" toml:"refresh_token,omitempty"`
15+
IDToken string `koanf:"id_token" yaml:"id_token,omitempty" json:"id_token,omitempty" toml:"id_token,omitempty"`
16+
TokenType string `koanf:"token_type" json:"token_type,omitempty" yaml:"token_type,omitempty" toml:"token_type,omitempty"`
17+
Scope string `koanf:"scope" json:"scope,omitempty" yaml:"scope,omitempty" toml:"scope,omitempty"`
18+
Expiry time.Time `koanf:"expiry" json:"expiry,omitempty" yaml:"expiry,omitempty" toml:"expiry,omitempty"`
1819
}

‎utilities/marshal.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package utilities
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/pelletier/go-toml"
8+
"gopkg.in/yaml.v3"
9+
10+
"authelia.com/tools/ac/consts"
11+
)
12+
13+
func AutoMarshal(v any, ext string) (data []byte, err error) {
14+
switch ext {
15+
case consts.FileTypeYAML, consts.FileTypeAltYAML:
16+
data, err = yaml.Marshal(v)
17+
case consts.FileTypeTOML, consts.FileTypeAltTOML:
18+
data, err = toml.Marshal(v)
19+
case consts.FileTypeJSON:
20+
data, err = json.MarshalIndent(v, "", " ")
21+
default:
22+
err = fmt.Errorf("extension '%s' is not supported", ext)
23+
}
24+
25+
return
26+
}

‎util.go renamed to ‎utilities/rand.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
package main
1+
package utilities
22

33
import (
44
"crypto/rand"
55
"fmt"
66
)
77

8-
func getRandomBytes(n int, charset []byte) (data []byte, err error) {
8+
func GetRandomBytes(n int, charset []byte) (data []byte, err error) {
99
data = make([]byte, n)
1010

1111
if _, err = rand.Read(data); err != nil {
@@ -24,5 +24,6 @@ func getRandomBytes(n int, charset []byte) (data []byte, err error) {
2424
}
2525

2626
var (
27-
charsetRFC3986Unreserved = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~")
27+
CharsetRFC3986Unreserved = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~")
28+
CharsetAlphaNumeric = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
2829
)

‎utilities/strings.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package utilities
2+
3+
// IsStringInSlice checks if a single string is in a slice of strings.
4+
func IsStringInSlice(needle string, haystack []string) (inSlice bool) {
5+
for _, b := range haystack {
6+
if b == needle {
7+
return true
8+
}
9+
}
10+
11+
return false
12+
}

0 commit comments

Comments
 (0)
Please sign in to comment.