Skip to content

Commit 0b60fe1

Browse files
authored
improve auth capabilities for notification client and Swagger UI (#304)
- add: notifications - Allow authentication via Client Credentials flow (breaking change) - change: make swagger docs compatible with access/authorization code flow ## Why We need to authenticate with client credentials in some scenarions. Also `Implicit` auth flow is deprecated and should not be used. So we need the Swagger UI with access/authorization code flow. ## References ARTOSI-258
2 parents d0bd8a1 + acb8264 commit 0b60fe1

File tree

11 files changed

+394
-102
lines changed

11 files changed

+394
-102
lines changed

.mockery.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
dir: "{{.InterfaceDir}}/mocks"
2+
pkgname: "mocks"
3+
structname: "{{.InterfaceName}}"
4+
filename: "{{.InterfaceName}}.go"
5+
all: true
6+
7+
packages:
8+
github.com/greenbone/opensight-golang-libraries/pkg/notifications:
9+
config:
10+
recursive: true

Makefile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ GO-MOD-UPGRADE = go run github.com/oligot/go-mod-upgrade@latest
2121
SWAG = github.com/swaggo/swag/cmd/swag@v1.16.2
2222

2323
INSTALL_GOMARKDOC = go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest
24-
INSTALL_MOCKERY = go install github.com/vektra/mockery/v2@v2.44.1
24+
MOCKERY = github.com/vektra/mockery/v3@v3.5.1
2525

2626
OS="$(shell go env var GOOS | xargs)"
2727

@@ -49,9 +49,11 @@ format: ## format and tidy
4949
go fmt ./...
5050

5151
generate-code: ## create mocks and enums
52-
@ echo "\033[36m Generate mocks and enums \033[0m"
52+
@ echo "\033[36m Generate enums \033[0m"
5353
go get github.com/abice/go-enum
5454
go generate ./...
55+
@ echo "\033[36m Generate mocks \033[0m"
56+
go run $(MOCKERY) --log-level warn
5557

5658

5759
.PHONY: lint
@@ -119,7 +121,7 @@ test-postgres:
119121
.PHONY: generate_docs
120122
generate_docs: check_tools
121123
gomarkdoc -e --output '{{.Dir}}/README.md' \
122-
--exclude-dirs .,./pkg/configReader/helper,./pkg/dbcrypt/config,./pkg/openSearch/openSearchClient/config \
124+
--exclude-dirs .,./pkg/configReader/helper,./pkg/dbcrypt/config,./pkg/openSearch/openSearchClient/config,./pkg/notifications/mocks \
123125
./pkg/...
124126

125127
check_tools:

pkg/auth/README.md

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,52 @@ Package auth provides a client to authenticate against a Keycloak server.
1212

1313
## Index
1414

15+
- [type ClientCredentials](<#ClientCredentials>)
16+
- [type Clock](<#Clock>)
17+
- [type Credentials](<#Credentials>)
1518
- [type KeycloakClient](<#KeycloakClient>)
16-
- [func NewKeycloakClient\(httpClient \*http.Client, cfg KeycloakConfig\) \*KeycloakClient](<#NewKeycloakClient>)
19+
- [func NewKeycloakClient\(httpClient \*http.Client, cfg KeycloakConfig, credentials Credentials\) \*KeycloakClient](<#NewKeycloakClient>)
1720
- [func \(c \*KeycloakClient\) GetToken\(ctx context.Context\) \(string, error\)](<#KeycloakClient.GetToken>)
1821
- [type KeycloakConfig](<#KeycloakConfig>)
22+
- [type ResourceOwnerCredentials](<#ResourceOwnerCredentials>)
1923

2024

25+
<a name="ClientCredentials"></a>
26+
## type [ClientCredentials](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L53-L56>)
27+
28+
ClientCredentials to authenticate via \`Client credentials grant\` flow. Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_client_credentials_grant
29+
30+
```go
31+
type ClientCredentials struct {
32+
ClientID string
33+
ClientSecret string
34+
}
35+
```
36+
37+
<a name="Clock"></a>
38+
## type [Clock](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L20-L22>)
39+
40+
41+
42+
```go
43+
type Clock interface {
44+
Now() time.Time
45+
}
46+
```
47+
48+
<a name="Credentials"></a>
49+
## type [Credentials](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L47-L49>)
50+
51+
Credentials holds the required credentials and determines the used auth type.
52+
53+
```go
54+
type Credentials interface {
55+
// contains filtered or unexported methods
56+
}
57+
```
58+
2159
<a name="KeycloakClient"></a>
22-
## type [KeycloakClient](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L42-L47>)
60+
## type [KeycloakClient](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L86-L94>)
2361

2462
KeycloakClient can be used to authenticate against a Keycloak server.
2563

@@ -30,38 +68,48 @@ type KeycloakClient struct {
3068
```
3169

3270
<a name="NewKeycloakClient"></a>
33-
### func [NewKeycloakClient](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L50>)
71+
### func [NewKeycloakClient](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L97>)
3472

3573
```go
36-
func NewKeycloakClient(httpClient *http.Client, cfg KeycloakConfig) *KeycloakClient
74+
func NewKeycloakClient(httpClient *http.Client, cfg KeycloakConfig, credentials Credentials) *KeycloakClient
3775
```
3876

39-
NewKeycloakClient creates a new KeycloakClient.
77+
NewKeycloakClient creates a new KeycloakClient. Passed [Credentials](<#Credentials>) determines the used auth type.
4078

4179
<a name="KeycloakClient.GetToken"></a>
42-
### func \(\*KeycloakClient\) [GetToken](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L60>)
80+
### func \(\*KeycloakClient\) [GetToken](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L107>)
4381

4482
```go
4583
func (c *KeycloakClient) GetToken(ctx context.Context) (string, error)
4684
```
4785

48-
GetToken retrieves a valid access token. The token is cached and refreshed before expiry. The token is obtained by \`Resource owner password credentials grant\` flow. Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_oidc-auth-flows-direct
86+
GetToken retrieves a valid access token. The token is cached and refreshed before expiry.
4987

5088
<a name="KeycloakConfig"></a>
51-
## type [KeycloakConfig](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L23-L29>)
89+
## type [KeycloakConfig](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L31-L34>)
5290

5391
KeycloakConfig holds the credentials and configuration details
5492

5593
```go
5694
type KeycloakConfig struct {
57-
ClientID string
58-
Username string
59-
Password string
6095
AuthURL string
6196
KeycloakRealm string
6297
}
6398
```
6499

100+
<a name="ResourceOwnerCredentials"></a>
101+
## type [ResourceOwnerCredentials](<https://github.com/greenbone/opensight-golang-libraries/blob/main/pkg/auth/auth_client.go#L60-L64>)
102+
103+
ResourceOwnerCredentials to authenticate via \`Resource owner password credentials grant\` flow. Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_oidc-auth-flows-direct
104+
105+
```go
106+
type ResourceOwnerCredentials struct {
107+
ClientID string
108+
Username string
109+
Password string
110+
}
111+
```
112+
65113
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
66114

67115

pkg/auth/auth_client.go

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,8 @@ const tokenRefreshMargin = 10 * time.Second
2929

3030
// KeycloakConfig holds the credentials and configuration details
3131
type KeycloakConfig struct {
32-
ClientID string
33-
Username string
34-
Password string //nolint:gosec
35-
AuthURL string
36-
KeycloakRealm string
32+
AuthURL string
33+
Realm string
3734
}
3835

3936
type tokenInfo struct {
@@ -46,28 +43,67 @@ type authResponse struct {
4643
ExpiresIn int `json:"expires_in"` // in seconds
4744
}
4845

46+
// Credentials holds the required credentials and determines the used auth type.
47+
type Credentials interface {
48+
constructUrlValues() url.Values // unexported method to prevent external implementation
49+
}
50+
51+
// ClientCredentials to authenticate via `Client credentials grant` flow.
52+
// Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_client_credentials_grant
53+
type ClientCredentials struct {
54+
ClientID string
55+
ClientSecret string
56+
}
57+
58+
func (c ClientCredentials) constructUrlValues() url.Values {
59+
urlValues := url.Values{}
60+
urlValues.Set("grant_type", "client_credentials")
61+
urlValues.Set("client_id", c.ClientID)
62+
urlValues.Set("client_secret", c.ClientSecret)
63+
64+
return urlValues
65+
}
66+
67+
// ResourceOwnerCredentials to authenticate via `Resource owner password credentials grant` flow.
68+
// Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_oidc-auth-flows-direct
69+
type ResourceOwnerCredentials struct {
70+
ClientID string
71+
Username string
72+
Password string
73+
}
74+
75+
func (c ResourceOwnerCredentials) constructUrlValues() url.Values {
76+
urlValues := url.Values{}
77+
urlValues.Set("grant_type", "password")
78+
urlValues.Set("client_id", c.ClientID)
79+
urlValues.Set("username", c.Username)
80+
urlValues.Set("password", c.Password)
81+
82+
return urlValues
83+
}
84+
4985
// KeycloakClient can be used to authenticate against a Keycloak server.
5086
type KeycloakClient struct {
51-
httpClient *http.Client
52-
cfg KeycloakConfig
53-
tokenInfo tokenInfo
54-
tokenMutex sync.RWMutex
87+
httpClient *http.Client
88+
cfg KeycloakConfig
89+
credentials Credentials
90+
tokenInfo tokenInfo
91+
tokenMutex sync.RWMutex
5592

5693
clock Clock // to mock time in tests
5794
}
5895

59-
// NewKeycloakClient creates a new KeycloakClient.
60-
func NewKeycloakClient(httpClient *http.Client, cfg KeycloakConfig) *KeycloakClient {
96+
// NewKeycloakClient creates a new KeycloakClient. Passed [Credentials] determines the used auth type.
97+
func NewKeycloakClient(httpClient *http.Client, cfg KeycloakConfig, credentials Credentials) *KeycloakClient {
6198
return &KeycloakClient{
62-
httpClient: httpClient,
63-
cfg: cfg,
64-
clock: realClock{},
99+
httpClient: httpClient,
100+
cfg: cfg,
101+
credentials: credentials,
102+
clock: realClock{},
65103
}
66104
}
67105

68106
// GetToken retrieves a valid access token. The token is cached and refreshed before expiry.
69-
// The token is obtained by `Resource owner password credentials grant` flow.
70-
// Ref: https://www.keycloak.org/docs/latest/server_admin/index.html#_oidc-auth-flows-direct
71107
func (c *KeycloakClient) GetToken(ctx context.Context) (string, error) {
72108
token, ok := c.getCachedToken()
73109
if ok {
@@ -106,14 +142,10 @@ func (c *KeycloakClient) getCachedToken() (token string, ok bool) {
106142
}
107143

108144
func (c *KeycloakClient) requestToken(ctx context.Context) (*authResponse, error) {
109-
data := url.Values{}
110-
data.Set("client_id", c.cfg.ClientID)
111-
data.Set("password", c.cfg.Password)
112-
data.Set("username", c.cfg.Username)
113-
data.Set("grant_type", "password")
145+
data := c.credentials.constructUrlValues()
114146

115147
authenticationURL := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/token",
116-
c.cfg.AuthURL, c.cfg.KeycloakRealm)
148+
c.cfg.AuthURL, c.cfg.Realm)
117149

118150
req, err := http.NewRequestWithContext(ctx, http.MethodPost, authenticationURL, strings.NewReader(data.Encode()))
119151
if err != nil {

pkg/auth/auth_client_test.go

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"net/http"
1111
"net/http/httptest"
12+
"net/url"
1213
"sync/atomic"
1314
"testing"
1415
"time"
@@ -19,21 +20,50 @@ import (
1920

2021
func TestKeycloakClient_GetToken(t *testing.T) {
2122
tests := map[string]struct {
22-
responseBody string
23-
responseCode int
24-
wantErr bool
25-
wantToken string
23+
credentials Credentials
24+
responseBody string
25+
responseCode int
26+
wantUrlValues url.Values
27+
wantErr bool
28+
wantToken string
2629
}{
27-
"successful token retrieval": {
30+
"successful token retrieval (client credentials)": {
31+
credentials: ClientCredentials{ClientID: "test-client-id", ClientSecret: "test-client-secret"},
32+
33+
responseBody: `{"access_token": "test-token", "expires_in": 3600}`,
34+
responseCode: http.StatusOK,
35+
wantUrlValues: url.Values{
36+
"grant_type": {"client_credentials"},
37+
"client_id": {"test-client-id"},
38+
"client_secret": {"test-client-secret"},
39+
},
40+
wantErr: false,
41+
wantToken: "test-token",
42+
},
43+
"successful token retrieval (resource owner credentials)": {
44+
credentials: ResourceOwnerCredentials{ClientID: "test-client-id", Username: "test-user", Password: "test-password"},
45+
2846
responseBody: `{"access_token": "test-token", "expires_in": 3600}`,
2947
responseCode: http.StatusOK,
30-
wantErr: false,
31-
wantToken: "test-token",
48+
wantUrlValues: url.Values{
49+
"grant_type": {"password"},
50+
"client_id": {"test-client-id"},
51+
"username": {"test-user"},
52+
"password": {"test-password"},
53+
},
54+
wantErr: false,
55+
wantToken: "test-token",
3256
},
3357
"failed authentication": {
58+
credentials: ClientCredentials{ClientID: "invalid-client-id", ClientSecret: "invalid-client-secret"},
3459
responseBody: `{}`,
3560
responseCode: http.StatusUnauthorized,
36-
wantErr: true,
61+
wantUrlValues: url.Values{
62+
"grant_type": {"client_credentials"},
63+
"client_id": {"invalid-client-id"},
64+
"client_secret": {"invalid-client-secret"},
65+
},
66+
wantErr: true,
3767
},
3868
}
3969
for name, tt := range tests {
@@ -43,16 +73,22 @@ func TestKeycloakClient_GetToken(t *testing.T) {
4373
w http.ResponseWriter, r *http.Request,
4474
) {
4575
serverCallCount.Add(1)
76+
77+
// Verify required URL parameters are present
78+
err := r.ParseForm()
79+
require.NoError(t, err)
80+
require.Equal(t, tt.wantUrlValues, r.Form)
81+
4682
w.WriteHeader(tt.responseCode)
47-
_, err := w.Write([]byte(tt.responseBody))
83+
_, err = w.Write([]byte(tt.responseBody))
4884
require.NoError(t, err)
4985
}))
5086
defer mockServer.Close()
5187

5288
client := NewKeycloakClient(http.DefaultClient, KeycloakConfig{
5389
AuthURL: mockServer.URL,
5490
// the other fields are also required in real scenario, but omit here for brevity
55-
})
91+
}, tt.credentials)
5692
gotToken, err := client.GetToken(context.Background())
5793
assert.Greater(t, serverCallCount.Load(), int32(0), "server was not called")
5894

@@ -123,7 +159,7 @@ func TestKeycloakClient_GetToken_Refresh(t *testing.T) {
123159
client := NewKeycloakClient(http.DefaultClient, KeycloakConfig{
124160
AuthURL: mockServer.URL,
125161
// the other fields are also required in real scenario, but omit here for brevity
126-
})
162+
}, ClientCredentials{})
127163
client.clock = fakeClock
128164

129165
_, err := client.GetToken(context.Background())

0 commit comments

Comments
 (0)