Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support tokens exchange via gRPC #12

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading