Skip to content

Commit ba715da

Browse files
authored
Merge pull request #434 from cisco-open/feat/custom_grant
Add support for custom token granter, enhancer, auth registry and tenant hierachy loader
2 parents 713e49d + 6f576bd commit ba715da

File tree

19 files changed

+478
-62
lines changed

19 files changed

+478
-62
lines changed

cmd/lanai-cli/initcmd/Dockerfile.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
ARG BASE_IMAGE=debian:bookworm
2-
ARG BUILDER_IMAGE=golang:1.21-bookworm
2+
ARG BUILDER_IMAGE=golang:1.24-bookworm
33

44
## Build Container ##
55
FROM ${BUILDER_IMAGE} AS builder

examples/auth/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ In this example, the source code contains the fully finished service. This step
1616
involved in writing this service.
1717

1818
### 1. Create the Initial Project
19-
This involves the following steps. For detail explanation of each step, see the [developer guide](../docs/Develop.md)
19+
This involves the following steps. For detail explanation of each step, see the [developer guide](../../docs/Develop.md)
2020

2121
1. Create Module.yml
2222
2. Add go.mod
2323
3. Add Makefile
24-
4. call ```shell make init CLI_TAG="develop"``` to initialize the project
24+
4. call ```shell make init CLI_TAG="main"``` to initialize the project
2525

2626
### 2. Adding Main File
2727
Add the main file corresponding to the definition in Module.yml. The main file is the entry point for this service.

examples/auth/pkg/service/clients.go

Lines changed: 0 additions & 1 deletion
This file was deleted.

pkg/integrate/httpclient/client.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ import (
2121
"errors"
2222
"fmt"
2323
"github.com/cisco-open/go-lanai/pkg/discovery"
24+
"github.com/cisco-open/go-lanai/pkg/utils"
2425
"github.com/cisco-open/go-lanai/pkg/utils/order"
26+
"net/url"
2527
"time"
2628
)
2729

2830
var (
2931
insecureInstanceMatcher = discovery.InstanceWithTagKV("secure", "false", true)
32+
supportedSchemes = utils.NewStringSet("http", "https")
3033
)
3134

3235
type clientDefaults struct {
@@ -203,9 +206,13 @@ func (c *client) shallowCopy() *client {
203206

204207
func (c *client) executor(request *Request, resolver TargetResolver, dec DecodeResponseFunc) Retryable {
205208
return func(ctx context.Context) (interface{}, error) {
206-
target, e := resolver.Resolve(ctx, request)
207-
if e != nil {
208-
return nil, e
209+
target, e := url.Parse(request.Path)
210+
// only need to resolve the target if the request.Path is not absolute
211+
if e != nil || !supportedSchemes.Has(target.Scheme) {
212+
target, e = resolver.Resolve(ctx, request)
213+
if e != nil {
214+
return nil, e
215+
}
209216
}
210217

211218
req, e := request.CreateFunc(ctx, request.Method, target)

pkg/integrate/httpclient/client_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"go.uber.org/fx"
3535
"net/http"
3636
"net/url"
37+
"path"
3738
"testing"
3839
"time"
3940
)
@@ -112,6 +113,7 @@ func TestWithMockedServer(t *testing.T) {
112113
test.GomegaSubTest(SubTestWithRetry(&di), "TestWithRetry"),
113114
test.GomegaSubTest(SubTestWithTimeout(&di), "TestWithTimeout"),
114115
test.GomegaSubTest(SubTestWithURLEncoded(&di), "TestWithURLEncoded"),
116+
test.GomegaSubTest(SubTestWithAbsoluteUrl(&di), "TestWithAbsoluteUrl"),
115117
)
116118
}
117119

@@ -390,6 +392,43 @@ func SubTestWithURLEncoded(di *TestDI) test.GomegaSubTestFunc {
390392
}
391393
}
392394

395+
func SubTestWithAbsoluteUrl(di *TestDI) test.GomegaSubTestFunc {
396+
return func(ctx context.Context, t *testing.T, g *gomega.WithT) {
397+
client := di.HttpClient
398+
399+
random := utils.RandomString(20)
400+
now := time.Now().Format(time.RFC3339)
401+
reqBody := makeEchoRequestBody()
402+
opts := append([]httpclient.RequestOptions{
403+
httpclient.WithHeader("X-Data", random),
404+
httpclient.WithParam("time", now),
405+
httpclient.WithParam("data", random),
406+
httpclient.WithBody(reqBody),
407+
})
408+
409+
uri, e := url.Parse(fmt.Sprintf(`http://localhost:%d%s`, webtest.CurrentPort(ctx), webtest.CurrentContextPath(ctx)))
410+
g.Expect(e).ToNot(HaveOccurred())
411+
412+
uri.Path = path.Join(uri.Path, TestPath)
413+
req := httpclient.NewRequest(uri.String(), http.MethodPost, opts...)
414+
415+
resp, e := client.Execute(ctx, req, httpclient.JsonBody(&EchoResponse{}))
416+
g.Expect(e).To(Succeed(), "execute request shouldn't fail")
417+
418+
expected := EchoResponse{
419+
Headers: map[string]string{
420+
"X-Data": random,
421+
},
422+
Form: map[string]string{
423+
"time": now,
424+
"data": random,
425+
},
426+
ReqBody: reqBody,
427+
}
428+
assertResponse(t, g, resp, http.StatusOK, &expected)
429+
}
430+
}
431+
393432
/*************************
394433
Request/Response
395434
*************************/

pkg/integrate/httpclient/context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ type Client interface {
4444
// The returned client is responsible to perform retrying.
4545
// The returned client is goroutine-safe and can be reused
4646
WithBaseUrl(baseUrl string) (Client, error)
47-
47+
4848
// WithConfig create a shallow copy of the client with specified config.
4949
// Service (with LB) or BaseURL cannot be changed with this method.
5050
// If any field of provided config is zero value, this value is not applied.

pkg/integrate/httpclient/resolver_static.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,3 @@ func NewStaticTargetResolver(baseUrl string) (TargetResolverFunc, error) {
2424
return &uri, nil
2525
}, nil
2626
}
27-
28-

pkg/security/config/authserver/config.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ type Configuration struct {
181181
OpenIDSSOEnabled bool
182182
SamlIdpSigningMethod string
183183
ApprovalStore auth.ApprovalStore
184+
CustomTokenGranter []auth.TokenGranter
185+
CustomTokenEnhancer []auth.TokenEnhancer
186+
CustomAuthRegistry auth.AuthorizationRegistry
184187

185188
// not directly configurable items
186189
appContext *bootstrap.ApplicationContext
@@ -265,6 +268,16 @@ func (c *Configuration) tokenGranter() auth.TokenGranter {
265268
granters = append(granters, passwordGranter)
266269
}
267270

271+
for _, custom := range c.CustomTokenGranter {
272+
switch v := custom.(type) {
273+
case auth.AuthorizationServiceInjector:
274+
v.Inject(c.authorizationService())
275+
default:
276+
// do nothing
277+
}
278+
}
279+
granters = append(granters, c.CustomTokenGranter...)
280+
268281
c.sharedTokenGranter = auth.NewCompositeTokenGranter(granters...)
269282
}
270283
return c.sharedTokenGranter
@@ -295,7 +308,11 @@ func (c *Configuration) contextDetailsStore() security.ContextDetailsStore {
295308

296309
func (c *Configuration) authorizationRegistry() auth.AuthorizationRegistry {
297310
if c.sharedAuthRegistry == nil {
298-
c.sharedAuthRegistry = c.contextDetailsStore().(auth.AuthorizationRegistry)
311+
if c.CustomAuthRegistry != nil {
312+
c.sharedAuthRegistry = c.CustomAuthRegistry
313+
} else {
314+
c.sharedAuthRegistry = c.contextDetailsStore().(auth.AuthorizationRegistry)
315+
}
299316
}
300317
return c.sharedAuthRegistry
301318
}
@@ -329,6 +346,7 @@ func (c *Configuration) authorizationService() auth.AuthorizationService {
329346
})
330347
conf.PostTokenEnhancers = append(conf.PostTokenEnhancers, openidEnhancer)
331348
}
349+
conf.TokenEnhancers = append(conf.TokenEnhancers, c.CustomTokenEnhancer...)
332350
})
333351
}
334352

pkg/security/config/integration_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/cisco-open/go-lanai/pkg/security/idp/passwdidp"
3838
"github.com/cisco-open/go-lanai/pkg/security/logout"
3939
"github.com/cisco-open/go-lanai/pkg/security/oauth2"
40+
"github.com/cisco-open/go-lanai/pkg/security/oauth2/auth"
4041
"github.com/cisco-open/go-lanai/pkg/security/oauth2/auth/authorize"
4142
"github.com/cisco-open/go-lanai/pkg/security/oauth2/auth/clientauth"
4243
"github.com/cisco-open/go-lanai/pkg/security/oauth2/auth/token"
@@ -59,6 +60,7 @@ import (
5960
. "github.com/cisco-open/go-lanai/test/utils/gomega"
6061
"github.com/cisco-open/go-lanai/test/webtest"
6162
"github.com/crewjam/saml"
63+
"github.com/golang-jwt/jwt/v4"
6264
"github.com/google/uuid"
6365
"github.com/onsi/gomega"
6466
. "github.com/onsi/gomega"
@@ -157,6 +159,7 @@ type intDI struct {
157159
Mocking sectest.MockingProperties
158160
TokenReader oauth2.TokenStoreReader
159161
SessionStore session.Store
162+
AuthReg auth.AuthorizationRegistry `optional:"true"`
160163
}
161164

162165
func TestWithMockedServer(t *testing.T) {
@@ -189,6 +192,7 @@ func TestWithMockedServer(t *testing.T) {
189192
testdata.NewAuthServerConfigurer, //This configurer will set up mocked client store, mocked tenant store etc.
190193
testdata.NewResServerConfigurer,
191194
testdata.NewMockedApprovalStore,
195+
auth.NewLegacyTokenEnhancer,
192196
),
193197
),
194198
test.GomegaSubTest(SubTestOAuth2AuthorizeWithPasswdIDP(di), "TestOAuth2AuthorizeWithPasswdIDP"),
@@ -234,6 +238,7 @@ func TestWithMockedServerWithoutFinalizer(t *testing.T) {
234238
sectest.BindMockingProperties,
235239
testdata.NewAuthServerConfigurer,
236240
testdata.NewResServerConfigurer,
241+
auth.NewLegacyTokenEnhancer,
237242
),
238243
),
239244
// a user has access to two tenants, switch from one to the other
@@ -242,6 +247,39 @@ func TestWithMockedServerWithoutFinalizer(t *testing.T) {
242247
)
243248
}
244249

250+
func TestWithMockedServerWithCustomTokenGranter(t *testing.T) {
251+
di := &intDI{}
252+
test.RunTest(context.Background(), t,
253+
apptest.Bootstrap(),
254+
apptest.WithTimeout(2*time.Minute),
255+
webtest.WithMockedServer(),
256+
sectest.WithMockedMiddleware(sectest.MWEnableSession()),
257+
apptest.WithModules(
258+
authserver.Module, resserver.Module,
259+
passwdidp.Module, extsamlidp.Module, authorize.Module, samlidp.Module,
260+
passwd.Module, formlogin.Module, logout.Module,
261+
samlctx.Module, samlsp.Module,
262+
basicauth.Module, clientauth.Module,
263+
token.Module, access.Module, errorhandling.Module,
264+
request_cache.Module, csrf.Module, session.Module,
265+
redis.Module,
266+
),
267+
apptest.WithDI(di),
268+
apptest.WithFxOptions(
269+
fx.Provide(
270+
IntegrationTestMocksProvider(),
271+
sectest.BindMockingProperties,
272+
testdata.NewAuthServerConfigurer,
273+
testdata.NewResServerConfigurer,
274+
testdata.NewCustomTokenEnhancer,
275+
testdata.NewCustomAuthRegistry,
276+
testdata.NewCustomTokenGranter,
277+
),
278+
),
279+
test.GomegaSubTest(SubTestCustomTokenGranter(di), "TestCustomTokenGranter"),
280+
)
281+
}
282+
245283
/*************************
246284
Sub Tests
247285
*************************/
@@ -914,6 +952,31 @@ func SubTestOauth2SwitchTenant(
914952
}
915953
}
916954

955+
func SubTestCustomTokenGranter(
956+
di *intDI,
957+
) test.GomegaSubTestFunc {
958+
return func(ctx context.Context, t *testing.T, g *gomega.WithT) {
959+
req := webtest.NewRequest(ctx, http.MethodPost, "/v2/token", customGrantReqBody(), withClientAuth("custom-grant-client", TestClientSecret), tokenReqOptions())
960+
resp := webtest.MustExec(ctx, req)
961+
g.Expect(resp).ToNot(BeNil(), "response should not be nil")
962+
g.Expect(resp.Response.StatusCode).To(Equal(http.StatusOK), "response should have correct status code")
963+
964+
body, e := io.ReadAll(resp.Response.Body)
965+
g.Expect(e).To(Succeed(), `token response body should be readable`)
966+
g.Expect(body).To(HaveJsonPath("$.access_token"), "token response should have access_token")
967+
968+
accessToken := oauth2.NewDefaultAccessToken("")
969+
e = json.Unmarshal(body, accessToken)
970+
g.Expect(e).ToNot(HaveOccurred())
971+
972+
tk, _, e := jwt.NewParser().ParseUnverified(accessToken.Value(), jwt.MapClaims{})
973+
g.Expect(e).ToNot(HaveOccurred())
974+
g.Expect(tk.Claims.(jwt.MapClaims)["MyClaim"]).To(Equal("my_claim_value"))
975+
976+
g.Expect(di.AuthReg.(*testdata.CustomAuthRegistry).RegistrationCount).To(Equal(1))
977+
}
978+
}
979+
917980
/*************************
918981
Helpers
919982
*************************/
@@ -1103,6 +1166,12 @@ func passwordGrantReqBody(tenantId string, username string, password string) io.
11031166
return strings.NewReader(values.Encode())
11041167
}
11051168

1169+
func customGrantReqBody() io.Reader {
1170+
values := url.Values{}
1171+
values.Set(oauth2.ParameterGrantType, "custom_grant")
1172+
return strings.NewReader(values.Encode())
1173+
}
1174+
11061175
func tokenReqOptions() webtest.RequestOptions {
11071176
return func(req *http.Request) {
11081177
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

pkg/security/config/testdata/application-test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ mocking:
9393
redirect-uris: ["localhost:*/**"]
9494
tenants: ["id-tenant-3"]
9595
scopes: "scope_a,scope_b"
96+
custom-grant-client:
97+
id: "custom-grant-client"
98+
secret: "test-secret"
99+
access-token-validity: 3600s
100+
grant-types: "custom_grant"
96101
accounts:
97102
system:
98103
username: "system"

0 commit comments

Comments
 (0)