Skip to content

Commit

Permalink
Support tokens exchange via gRPC
Browse files Browse the repository at this point in the history
  • Loading branch information
vasayxtx committed Oct 30, 2024
1 parent a7f0727 commit cd4181a
Show file tree
Hide file tree
Showing 20 changed files with 905 additions and 649 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
cache: false

- name: Run GolangCI-Lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
version: v1.56.1
version: v1.61.0
args: --timeout=5m
43 changes: 17 additions & 26 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func NewTokenIntrospector(
tokenProvider idptoken.IntrospectionTokenProvider,
scopeFilter []idptoken.IntrospectionScopeFilterAccessPolicy,
opts ...TokenIntrospectorOption,
) (TokenIntrospector, error) {
) (*idptoken.Introspector, error) {
var options tokenIntrospectorOptions
for _, opt := range opts {
opt(&options)
Expand All @@ -147,43 +147,34 @@ func NewTokenIntrospector(
}

introspectorOpts := idptoken.IntrospectorOpts{
StaticHTTPEndpoint: cfg.Introspection.Endpoint,
HTTPEndpoint: cfg.Introspection.Endpoint,
GRPCClient: grpcClient,
HTTPClient: idputil.MakeDefaultHTTPClient(cfg.HTTPClient.RequestTimeout, logger),
AccessTokenScope: cfg.Introspection.AccessTokenScope,
Logger: logger,
ScopeFilter: scopeFilter,
TrustedIssuerNotFoundFallback: options.trustedIssuerNotFoundFallback,
PrometheusLibInstanceLabel: options.prometheusLibInstanceLabel,
ClaimsCache: idptoken.IntrospectorCacheOpts{
Enabled: cfg.Introspection.ClaimsCache.Enabled,
MaxEntries: cfg.Introspection.ClaimsCache.MaxEntries,
TTL: cfg.Introspection.ClaimsCache.TTL,
},
NegativeCache: idptoken.IntrospectorCacheOpts{
Enabled: cfg.Introspection.NegativeCache.Enabled,
MaxEntries: cfg.Introspection.NegativeCache.MaxEntries,
TTL: cfg.Introspection.NegativeCache.TTL,
},
}

if cfg.Introspection.ClaimsCache.Enabled || cfg.Introspection.NegativeCache.Enabled {
cachingIntrospector, err := idptoken.NewCachingIntrospectorWithOpts(tokenProvider, idptoken.CachingIntrospectorOpts{
IntrospectorOpts: introspectorOpts,
ClaimsCache: idptoken.CachingIntrospectorCacheOpts{
Enabled: cfg.Introspection.ClaimsCache.Enabled,
MaxEntries: cfg.Introspection.ClaimsCache.MaxEntries,
TTL: cfg.Introspection.ClaimsCache.TTL,
},
NegativeCache: idptoken.CachingIntrospectorCacheOpts{
Enabled: cfg.Introspection.NegativeCache.Enabled,
MaxEntries: cfg.Introspection.NegativeCache.MaxEntries,
TTL: cfg.Introspection.NegativeCache.TTL,
},
})
if err != nil {
return nil, fmt.Errorf("new caching introspector: %w", err)
}
if err = addTrustedIssuers(cachingIntrospector, cfg.JWT.TrustedIssuers, cfg.JWT.TrustedIssuerURLs); err != nil {
return nil, err
}
return cachingIntrospector, nil
introspector, err := idptoken.NewIntrospectorWithOpts(tokenProvider, introspectorOpts)
if err != nil {
return nil, err
}

introspector := idptoken.NewIntrospectorWithOpts(tokenProvider, introspectorOpts)
if err := addTrustedIssuers(introspector, cfg.JWT.TrustedIssuers, cfg.JWT.TrustedIssuerURLs); err != nil {
if err = addTrustedIssuers(introspector, cfg.JWT.TrustedIssuers, cfg.JWT.TrustedIssuerURLs); err != nil {
return nil, err
}

return introspector, nil
}

Expand Down
53 changes: 28 additions & 25 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/acronis/go-authkit/idptest"
"github.com/acronis/go-authkit/idptoken"
"github.com/acronis/go-authkit/idptoken/pb"
"github.com/acronis/go-authkit/internal/idputil"
"github.com/acronis/go-authkit/internal/testing"
"github.com/acronis/go-authkit/jwt"
)
Expand Down Expand Up @@ -126,9 +127,12 @@ func TestNewJWTParser(t *gotesting.T) {

func TestNewTokenIntrospector(t *gotesting.T) {
const testIss = "test-issuer"
const validAccessToken = "access-token-with-introspection-permission"

httpServerIntrospector := testing.NewHTTPServerTokenIntrospectorMock()
httpServerIntrospector.SetAccessTokenForIntrospection(validAccessToken)
grpcServerIntrospector := testing.NewGRPCServerTokenIntrospectorMock()
grpcServerIntrospector.SetAccessTokenForIntrospection(validAccessToken)

// Start testing HTTP IDP server.
httpIDPSrv := idptest.NewHTTPServer(idptest.WithHTTPTokenIntrospector(httpServerIntrospector))
Expand Down Expand Up @@ -171,9 +175,9 @@ func TestNewTokenIntrospector(t *gotesting.T) {
ResourcePath: "resource-" + uuid.NewString(),
}}
httpServerIntrospector.SetResultForToken(opaqueToken, idptoken.IntrospectionResult{
Active: true, TokenType: idptoken.TokenTypeBearer, Claims: jwt.Claims{Scope: opaqueTokenScope}})
Active: true, TokenType: idputil.TokenTypeBearer, Claims: jwt.Claims{Scope: opaqueTokenScope}})
grpcServerIntrospector.SetResultForToken(opaqueToken, &pb.IntrospectTokenResponse{
Active: true, TokenType: idptoken.TokenTypeBearer, Scope: []*pb.AccessTokenScope{
Active: true, TokenType: idputil.TokenTypeBearer, Scope: []*pb.AccessTokenScope{
{
TenantUuid: opaqueTokenScope[0].TenantUUID,
ResourceNamespace: opaqueTokenScope[0].ResourceNamespace,
Expand All @@ -187,7 +191,7 @@ func TestNewTokenIntrospector(t *gotesting.T) {
cfg *Config
token string
expectedResult idptoken.IntrospectionResult
checkFn func(t *gotesting.T, introspector TokenIntrospector)
checkCacheFn func(t *gotesting.T, introspector *idptoken.Introspector)
}{
{
name: "new token introspector, dynamic endpoint, trusted issuers map",
Expand All @@ -198,8 +202,9 @@ func TestNewTokenIntrospector(t *gotesting.T) {
TokenType: "bearer",
Claims: *claimsWithNamedIssuer,
},
checkFn: func(t *gotesting.T, introspector TokenIntrospector) {
require.IsType(t, &idptoken.Introspector{}, introspector)
checkCacheFn: func(t *gotesting.T, introspector *idptoken.Introspector) {
require.Empty(t, introspector.ClaimsCache.Len(context.Background()))
require.Empty(t, introspector.NegativeCache.Len(context.Background()))
},
},
{
Expand All @@ -211,8 +216,9 @@ func TestNewTokenIntrospector(t *gotesting.T) {
TokenType: "bearer",
Claims: *claims,
},
checkFn: func(t *gotesting.T, introspector TokenIntrospector) {
require.IsType(t, &idptoken.Introspector{}, introspector)
checkCacheFn: func(t *gotesting.T, introspector *idptoken.Introspector) {
require.Empty(t, introspector.ClaimsCache.Len(context.Background()))
require.Empty(t, introspector.NegativeCache.Len(context.Background()))
},
},
{
Expand All @@ -227,10 +233,9 @@ func TestNewTokenIntrospector(t *gotesting.T) {
TokenType: "bearer",
Claims: *claimsWithNamedIssuer,
},
checkFn: func(t *gotesting.T, introspector TokenIntrospector) {
require.IsType(t, &idptoken.CachingIntrospector{}, introspector)
cachingIntrospector := introspector.(*idptoken.CachingIntrospector)
require.Equal(t, 1, cachingIntrospector.ClaimsCache.Len(context.Background()))
checkCacheFn: func(t *gotesting.T, introspector *idptoken.Introspector) {
require.Equal(t, 1, introspector.ClaimsCache.Len(context.Background()))
require.Empty(t, introspector.NegativeCache.Len(context.Background()))
},
},
{
Expand All @@ -245,10 +250,9 @@ func TestNewTokenIntrospector(t *gotesting.T) {
TokenType: "bearer",
Claims: *claims,
},
checkFn: func(t *gotesting.T, introspector TokenIntrospector) {
require.IsType(t, &idptoken.CachingIntrospector{}, introspector)
cachingIntrospector := introspector.(*idptoken.CachingIntrospector)
require.Equal(t, 1, cachingIntrospector.ClaimsCache.Len(context.Background()))
checkCacheFn: func(t *gotesting.T, introspector *idptoken.Introspector) {
require.Equal(t, 1, introspector.ClaimsCache.Len(context.Background()))
require.Empty(t, introspector.NegativeCache.Len(context.Background()))
},
},
{
Expand All @@ -266,10 +270,9 @@ func TestNewTokenIntrospector(t *gotesting.T) {
TokenType: "bearer",
Claims: jwt.Claims{Scope: opaqueTokenScope},
},
checkFn: func(t *gotesting.T, introspector TokenIntrospector) {
require.IsType(t, &idptoken.CachingIntrospector{}, introspector)
cachingIntrospector := introspector.(*idptoken.CachingIntrospector)
require.Equal(t, 1, cachingIntrospector.ClaimsCache.Len(context.Background()))
checkCacheFn: func(t *gotesting.T, introspector *idptoken.Introspector) {
require.Equal(t, 1, introspector.ClaimsCache.Len(context.Background()))
require.Empty(t, introspector.NegativeCache.Len(context.Background()))
},
},
{
Expand All @@ -285,7 +288,6 @@ func TestNewTokenIntrospector(t *gotesting.T) {
CACert: certFile,
},
},
Endpoint: httpIDPSrv.URL() + idptest.TokenIntrospectionEndpointPath,
},
},
token: opaqueToken,
Expand All @@ -294,8 +296,9 @@ func TestNewTokenIntrospector(t *gotesting.T) {
TokenType: "bearer",
Claims: jwt.Claims{Scope: opaqueTokenScope},
},
checkFn: func(t *gotesting.T, introspector TokenIntrospector) {
require.IsType(t, &idptoken.Introspector{}, introspector)
checkCacheFn: func(t *gotesting.T, introspector *idptoken.Introspector) {
require.Empty(t, introspector.ClaimsCache.Len(context.Background()))
require.Empty(t, introspector.NegativeCache.Len(context.Background()))
},
},
}
Expand All @@ -306,14 +309,14 @@ func TestNewTokenIntrospector(t *gotesting.T) {
httpServerIntrospector.JWTParser = jwtParser
grpcServerIntrospector.JWTParser = jwtParser

introspector, err := NewTokenIntrospector(tt.cfg, idptest.NewSimpleTokenProvider("access-token"), nil)
introspector, err := NewTokenIntrospector(tt.cfg, idptest.NewSimpleTokenProvider(validAccessToken), nil)
require.NoError(t, err)

result, err := introspector.IntrospectToken(context.Background(), tt.token)
require.NoError(t, err)
require.Equal(t, tt.expectedResult, result)
if tt.checkFn != nil {
tt.checkFn(t, introspector)
if tt.checkCacheFn != nil {
tt.checkCacheFn(t, introspector)
}
})
}
Expand Down
6 changes: 6 additions & 0 deletions examples/authn-middleware/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/*
Copyright © 2024 Acronis International GmbH.
Released under MIT license.
*/

package main

import (
Expand Down
8 changes: 7 additions & 1 deletion examples/idp-test-server/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
/*
Copyright © 2024 Acronis International GmbH.
Released under MIT license.
*/

package main

import (
"context"
"errors"
"github.com/acronis/go-authkit"
golog "log"
"net/http"
"os"
Expand All @@ -16,6 +21,7 @@ import (
jwtgo "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"

"github.com/acronis/go-authkit"
"github.com/acronis/go-authkit/idptest"
"github.com/acronis/go-authkit/idptoken"
"github.com/acronis/go-authkit/jwks"
Expand Down
36 changes: 27 additions & 9 deletions examples/token-introspection/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/*
Copyright © 2024 Acronis International GmbH.
Released under MIT license.
*/

package main

import (
Expand All @@ -10,9 +16,9 @@ import (
"github.com/acronis/go-appkit/config"
"github.com/acronis/go-appkit/httpserver/middleware"
"github.com/acronis/go-appkit/log"
"github.com/acronis/go-authkit/idptoken"

"github.com/acronis/go-authkit"
"github.com/acronis/go-authkit/idptoken"
)

const (
Expand All @@ -36,39 +42,51 @@ func runApp() error {
logger, loggerClose := log.NewLogger(cfg.Log)
defer loggerClose()

// create JWT parser and token introspector
// Create JWT parser.
jwtParser, err := authkit.NewJWTParser(cfg.Auth, authkit.WithJWTParserLogger(logger))
if err != nil {
return fmt.Errorf("create JWT parser: %w", err)
}

// Create token introspector.
introspectionScopeFilter := []idptoken.IntrospectionScopeFilterAccessPolicy{
{ResourceNamespace: serviceAccessPolicy}}
tokenIntrospector, err := authkit.NewTokenIntrospector(cfg.Auth, introspectionTokenProvider{},
introspectionScopeFilter, authkit.WithTokenIntrospectorLogger(logger))
if err != nil {
return fmt.Errorf("create token introspector: %w", err)
}
if tokenIntrospector.GRPCClient != nil {
defer func() {
if closeErr := tokenIntrospector.GRPCClient.Close(); closeErr != nil {
logger.Error("failed to close gRPC client", log.Error(closeErr))
}
}()
}

logMw := middleware.Logging(logger)

// configure JWTAuthMiddleware that performs only authentication via OAuth2 token introspection endpoint
// Configure JWTAuthMiddleware that performs only authentication via OAuth2 token introspection endpoint.
authNMw := authkit.JWTAuthMiddleware(serviceErrorDomain, jwtParser,
authkit.WithJWTAuthMiddlewareTokenIntrospector(tokenIntrospector))

// configure JWTAuthMiddleware that performs authentication via token introspection endpoint
// and authorization based on the user's roles
// Configure JWTAuthMiddleware that performs authentication via token introspection endpoint
// and authorization based on the user's roles.
authZMw := authkit.JWTAuthMiddleware(serviceErrorDomain, jwtParser,
authkit.WithJWTAuthMiddlewareTokenIntrospector(tokenIntrospector),
authkit.WithJWTAuthMiddlewareVerifyAccess(
authkit.NewVerifyAccessByRolesInJWT(authkit.Role{Namespace: serviceAccessPolicy, Name: "admin"})))

// create HTTP server and start it
// Create HTTP server and start it.
srvMux := http.NewServeMux()
// "/" endpoint will be available for all authenticated users
// "/" endpoint will be available for all authenticated users.
srvMux.Handle("/", logMw(authNMw(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
jwtClaims := authkit.GetJWTClaimsFromContext(r.Context()) // get JWT claims from the request context
_, _ = rw.Write([]byte(fmt.Sprintf("Hello, %s", jwtClaims.Subject)))
}))))
// "/admin" endpoint will be available only for users with the "admin" role
// "/admin" endpoint will be available only for users with the "admin" role.
srvMux.Handle("/admin", logMw(authZMw(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
jwtClaims := authkit.GetJWTClaimsFromContext(r.Context()) // get JWT claims from the request context
jwtClaims := authkit.GetJWTClaimsFromContext(r.Context()) // Get JWT claims from the request context.
_, _ = rw.Write([]byte(fmt.Sprintf("Hi, %s", jwtClaims.Subject)))
}))))
if err = http.ListenAndServe(":8080", srvMux); err != nil && !errors.Is(err, http.ErrServerClosed) {
Expand Down
4 changes: 2 additions & 2 deletions idptest/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
jwtgo "github.com/golang-jwt/jwt/v5"
"github.com/mendsley/gojwk"

"github.com/acronis/go-authkit/idptoken"
"github.com/acronis/go-authkit/internal/idputil"
)

// SignToken signs token with key.
Expand All @@ -36,7 +36,7 @@ func MakeTokenStringWithHeader(
claims jwtgo.Claims, kid string, rsaPrivateKey interface{}, header map[string]interface{},
) (string, error) {
token := jwtgo.NewWithClaims(jwtgo.SigningMethodRS256, claims)
token.Header["typ"] = idptoken.JWTTypeAccessToken
token.Header["typ"] = idputil.JWTTypeAccessToken
token.Header["kid"] = kid
for k, v := range header {
token.Header[k] = v
Expand Down
Loading

0 comments on commit cd4181a

Please sign in to comment.