Skip to content
Merged
Show file tree
Hide file tree
Changes from 97 commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
5f4b064
feat: verify and validate access tokens on service calls
strantalis Feb 27, 2024
c952554
add autnInterceptor
strantalis Feb 27, 2024
5358a10
save
strantalis Feb 29, 2024
6a1c022
Merge branch 'main' into feat/authn-support
strantalis Feb 29, 2024
f6b84c1
save progress
strantalis Mar 1, 2024
2d38e69
remove testIDP var
strantalis Mar 1, 2024
d7e4b82
cleanup
strantalis Mar 1, 2024
7aa3273
comments
strantalis Mar 1, 2024
e99dcfc
comment
strantalis Mar 1, 2024
68d96f9
unit tests for access token verification and validation
strantalis Mar 2, 2024
f361f20
Merge branch 'main' into feat/authn-support
strantalis Mar 2, 2024
10be45c
updated configuration docs
strantalis Mar 2, 2024
7bb68e3
registered authn check as handler in mux chain
strantalis Mar 2, 2024
717485f
rename authN config field and remove left over log line
strantalis Mar 6, 2024
43db635
Merge branch 'main' into feat/authn-support
strantalis Mar 6, 2024
7d783cd
Merge branch 'main' into feat/authn-support
strantalis Mar 7, 2024
42f7daf
move authn to internal
strantalis Mar 7, 2024
72791c7
Merge branch 'main' into feat/authn-support
strantalis Mar 7, 2024
3394384
fix authn test
strantalis Mar 7, 2024
0c54866
only set issuer in platform welknown config
strantalis Mar 7, 2024
99c3153
fix loading authn with handler
strantalis Mar 7, 2024
f2947b3
fix grpccurl step
strantalis Mar 7, 2024
510562f
fix healthcheck grpccurl call
strantalis Mar 7, 2024
a0f6e75
didn't save
strantalis Mar 7, 2024
58d209c
disable auth for service extension test
strantalis Mar 7, 2024
0f8ce82
need to set mux to handler on server start
strantalis Mar 7, 2024
f29f471
Merge branch 'main' into feat/authn-support
strantalis Mar 7, 2024
629ad2f
try nohub
strantalis Mar 7, 2024
5091e2f
try just go start to see errors
strantalis Mar 7, 2024
2c03635
pause on starting opentdf
strantalis Mar 7, 2024
b077792
disable auth in example config
strantalis Mar 7, 2024
29abfbb
Merge branch 'main' into feat/authn-support
strantalis Mar 7, 2024
1599c15
Update internal/auth/authn.go
strantalis Mar 7, 2024
d93e257
Merge branch 'main' into feat/authn-support
strantalis Mar 7, 2024
1169de3
Merge branch 'feat/authn-support' of github.com:opentdf/opentdf-v2-po…
strantalis Mar 7, 2024
acd90fd
Update internal/server/server.go
strantalis Mar 8, 2024
b7b034d
Merge branch 'main' into feat/authn-support
strantalis Mar 8, 2024
eb3ee6f
take a function so that callers can use this the way that they want
mkleene Mar 8, 2024
fac35c5
rename
mkleene Mar 8, 2024
83e2c42
add this
mkleene Mar 8, 2024
12dc95b
Revert "fix(sdk): temporarily move unwrapper creation into options fu…
mkleene Mar 8, 2024
bdace36
this seems a little cleaner to me
mkleene Mar 8, 2024
1d7d5cd
Merge remote-tracking branch 'origin/main' into change-unwrapper-crea…
mkleene Mar 8, 2024
48042b9
add comment and better assertion
mkleene Mar 8, 2024
169fdb7
Merge remote-tracking branch 'origin/change-unwrapper-creation' into …
mkleene Mar 8, 2024
c4dd18f
adding a token appears to work from the client side
mkleene Mar 8, 2024
cf2ce7b
Merge remote-tracking branch 'origin/main' into include-auth-token-in…
mkleene Mar 8, 2024
24a6727
Merge branch 'main' into feat/authn-support
strantalis Mar 11, 2024
0b8bf9d
add interceptor to add tokens to outgoing client requests
mkleene Mar 11, 2024
7200b61
we don't need duplicates
mkleene Mar 11, 2024
7558c97
move this
mkleene Mar 11, 2024
3a83cef
lint
mkleene Mar 11, 2024
0cb810d
Update token_adding_interceptor_test.go
mkleene Mar 11, 2024
8fdc8f6
Merge branch 'main' into include-auth-token-in-grpc
mkleene Mar 11, 2024
23eec93
code review points
mkleene Mar 11, 2024
673a1a1
Merge remote-tracking branch 'origin/include-auth-token-in-grpc' into…
mkleene Mar 11, 2024
78002c9
rename
mkleene Mar 11, 2024
96961c4
add DPoP validation
mkleene Mar 13, 2024
63edb54
Merge remote-tracking branch 'origin/main' into include-auth-token-in…
mkleene Mar 13, 2024
161b356
Merge branch 'feat/authn-support' into include-auth-token-in-grpc
mkleene Mar 13, 2024
4402da2
Merge remote-tracking branch 'origin/main' into include-auth-token-in…
mkleene Mar 13, 2024
f4ee37d
hide the `AsymDecryption` so that we can move the interface
mkleene Mar 13, 2024
d6bc47f
move the interface
mkleene Mar 13, 2024
4f7a68e
add some testing
mkleene Mar 14, 2024
5af9bdf
Merge remote-tracking branch 'origin/main' into include-auth-token-in…
mkleene Mar 14, 2024
479a477
not needed
mkleene Mar 14, 2024
020124c
do not want to change this
mkleene Mar 14, 2024
5149e25
not needed
mkleene Mar 14, 2024
a962511
add new files
mkleene Mar 14, 2024
c3af3e9
Delete sdk/token_adding_interceptor.go
mkleene Mar 14, 2024
74127ac
Delete sdk/token_adding_interceptor_test.go
mkleene Mar 14, 2024
0223827
name change
mkleene Mar 14, 2024
706106b
go mod tidy
mkleene Mar 14, 2024
8b86be9
wire this into the sdk
mkleene Mar 14, 2024
3a71108
oops
mkleene Mar 14, 2024
611cb80
missed a rename
mkleene Mar 14, 2024
369ae7d
see if this works
mkleene Mar 14, 2024
253c285
lint
mkleene Mar 14, 2024
c740e4a
lint
mkleene Mar 14, 2024
f2b50d8
more lint
mkleene Mar 14, 2024
d2b6706
more lint
mkleene Mar 14, 2024
5c655f4
Merge branch 'main' into include-auth-token-in-grpc
mkleene Mar 14, 2024
8e67b1a
Merge branch 'main' into include-auth-token-in-grpc
mkleene Mar 14, 2024
d8b3b9f
if they give us a token with a `cnf` reject it
mkleene Mar 15, 2024
c6588df
Merge remote-tracking branch 'origin/include-auth-token-in-grpc' into…
mkleene Mar 15, 2024
52dcf76
no need to add the DPoP token here
mkleene Mar 15, 2024
72427db
Merge branch 'main' into include-auth-token-in-grpc
mkleene Mar 15, 2024
3706ee6
move stuff and change the tests
mkleene Mar 18, 2024
6736b4a
use better methods
mkleene Mar 18, 2024
bbbc34d
add e2e test
mkleene Mar 18, 2024
6234d40
Merge remote-tracking branch 'origin/main' into include-auth-token-in…
mkleene Mar 18, 2024
72f4042
lint
mkleene Mar 18, 2024
75f7263
test
mkleene Mar 18, 2024
a7b06e5
see if that shows the line numbers
mkleene Mar 18, 2024
e3fe2ff
Revert "see if that shows the line numbers"
mkleene Mar 18, 2024
6dbe49f
just do not cast
mkleene Mar 18, 2024
573e4cf
Merge branch 'main' into include-auth-token-in-grpc
mkleene Mar 18, 2024
86bccba
refactor: Remove deps on backend-go
dmihalcik-virtru Mar 19, 2024
b7f1087
chore: DPoP capitalization nits
dmihalcik-virtru Mar 19, 2024
cf6425c
refactor: lets a function be a method
dmihalcik-virtru Mar 19, 2024
1a4e78d
refactor: reduce a warn logline to info
dmihalcik-virtru Mar 19, 2024
2dba7b8
chore: lint cleanup
dmihalcik-virtru Mar 19, 2024
4b7d915
chore: lint fixes
dmihalcik-virtru Mar 19, 2024
17e7046
ci: Fix invalid test
dmihalcik-virtru Mar 19, 2024
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ require (
github.com/valyala/fasthttp v1.52.0
github.com/virtru/access-pdp v1.11.0
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
golang.org/x/oauth2 v0.18.0
google.golang.org/grpc v1.62.1
google.golang.org/protobuf v1.33.0
gopkg.in/go-jose/go-jose.v2 v2.6.3
Expand Down Expand Up @@ -140,6 +139,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.18.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
Expand Down
2 changes: 1 addition & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"github.com/go-chi/cors"
protovalidate_middleware "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/opentdf/platform/internal/auth"
"github.com/opentdf/platform/sdk/auth"
"github.com/valyala/fasthttp/fasthttputil"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
Expand Down
2 changes: 1 addition & 1 deletion pkg/server/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import (
"time"

"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/opentdf/platform/internal/auth"
"github.com/opentdf/platform/internal/config"
"github.com/opentdf/platform/internal/server"
"github.com/opentdf/platform/pkg/serviceregistry"
"github.com/opentdf/platform/sdk/auth"
"github.com/stretchr/testify/assert"
"golang.org/x/exp/slog"
"google.golang.org/grpc"
Expand Down
15 changes: 15 additions & 0 deletions sdk/auth/access_token_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package auth

import "github.com/lestrrat-go/jwx/v2/jwk"

type AccessToken string

type AccessTokenSource interface {
AccessToken() (AccessToken, error)
// probably better to use `crypto.AsymDecryption` here than roll our own since this should be
// more closely linked to what happens in KAS in terms of crypto params
DecryptWithDPoPKey(data []byte) ([]byte, error)
MakeToken(func(jwk.Key) ([]byte, error)) ([]byte, error)
DPOPPublicKeyPEM() string
RefreshAccessToken() error
}
187 changes: 169 additions & 18 deletions internal/auth/authn.go → sdk/auth/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ package auth

import (
"context"
"crypto"
"crypto/sha256"
"encoding/base64"
"fmt"
"log/slog"
"net/http"
"slices"
"strings"
"time"

"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
Expand All @@ -28,6 +33,18 @@ var (
"/healthz",
"/.well-known/opentdf-configuration",
}
// only asymmetric algorithms and no 'none'
allowedSignatureAlgorithms = map[jwa.SignatureAlgorithm]bool{
jwa.RS256: true,
jwa.RS384: true,
jwa.RS512: true,
jwa.ES256: true,
jwa.ES384: true,
jwa.ES512: true,
jwa.PS256: true,
jwa.PS384: true,
jwa.PS512: true,
}
)

// Authentication holds a jwks cache and information about the openid configuration
Expand Down Expand Up @@ -72,6 +89,12 @@ func NewAuthenticator(cfg AuthNConfig) (*authentication, error) {
return a, nil
}

type dpopInfo struct {
headers []string
path string
method string
}

// verifyTokenHandler is a http handler that verifies the token
func (a authentication) VerifyTokenHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -85,7 +108,12 @@ func (a authentication) VerifyTokenHandler(handler http.Handler) http.Handler {
http.Error(w, "missing authorization header", http.StatusUnauthorized)
return
}
err := checkToken(r.Context(), header, a)
dpopInfo := dpopInfo{
headers: r.Header["Dpop"],
path: r.URL.Path,
method: r.Method,
}
err := checkToken(r.Context(), header, dpopInfo, a)
if err != nil {
slog.WarnContext(r.Context(), "failed to validate token", slog.String("error", err.Error()))
http.Error(w, "unauthenticated", http.StatusUnauthorized)
Expand Down Expand Up @@ -117,7 +145,13 @@ func (a authentication) VerifyTokenInterceptor(ctx context.Context, req any, inf
return nil, status.Error(codes.Unauthenticated, "missing authorization header")
}

err := checkToken(ctx, header, a)
dpopInfo := dpopInfo{
headers: md["dpop"],
path: info.FullMethod,
method: "POST",
}

err := checkToken(ctx, header, dpopInfo, a)
if err != nil {
slog.Warn("failed to validate token", slog.String("error", err.Error()))
return nil, status.Errorf(codes.Unauthenticated, "unauthenticated")
Expand All @@ -127,7 +161,7 @@ func (a authentication) VerifyTokenInterceptor(ctx context.Context, req any, inf
}

// checkToken is a helper function to verify the token.
func checkToken(ctx context.Context, authHeader []string, auth authentication) error {
func checkToken(ctx context.Context, authHeader []string, dpopInfo dpopInfo, auth authentication) error {
var (
tokenRaw string
tokenType string
Expand All @@ -145,32 +179,22 @@ func checkToken(ctx context.Context, authHeader []string, auth authentication) e
return fmt.Errorf("not of type bearer or dpop")
}

// Future work is to validate DPoP proof if token type is DPoP
//nolint:staticcheck
if tokenType == "DPoP" {
// Implement in the future here or as separate interceptor
}

// We have to get iss from the token first to verify the signature
unverifiedToken, err := jwt.Parse([]byte(tokenRaw), jwt.WithVerify(false))
if err != nil {
return err
}

// Get issuer from unverified token
issuer, exists := unverifiedToken.Get("iss")
if !exists {
issuer := unverifiedToken.Issuer()
if issuer == "" {
return fmt.Errorf("missing issuer")
}

// Get the openid configuration for the issuer
// Because we get an interface we need to cast it to a string
// and jwx expects it as a string so we should never hit this error if the token is valid
issuerStr, ok := issuer.(string)
if !ok {
return fmt.Errorf("invalid issuer")
}
oidc, exists := auth.oidcConfigurations[issuerStr]
oidc, exists := auth.oidcConfigurations[issuer]
if !exists {
return fmt.Errorf("invalid issuer")
}
Expand All @@ -182,17 +206,144 @@ func checkToken(ctx context.Context, authHeader []string, auth authentication) e
}

// Now we verify the token signature
_, err = jwt.Parse([]byte(tokenRaw),
accessToken, err := jwt.Parse([]byte(tokenRaw),
jwt.WithKeySet(keySet),
jwt.WithValidate(true),
jwt.WithIssuer(issuerStr),
jwt.WithIssuer(issuer),
jwt.WithAudience(oidc.Audience),
jwt.WithValidator(jwt.ValidatorFunc(auth.claimsValidator)),
)

if err != nil {
return err
}

if tokenType == "Bearer" {
if _, ok := accessToken.Get("cnf"); !ok {
return nil
}
slog.Warn("presented token with `cnf` claim as a bearer token. validating as DPoP")
}

return validateDPoP(accessToken, tokenRaw, dpopInfo)
}

func validateDPoP(accessToken jwt.Token, acessTokenRaw string, dpopInfo dpopInfo) error {
if len(dpopInfo.headers) != 1 {
return fmt.Errorf("got %d dpop headers, should have 1", len(dpopInfo.headers))
}
dpopHeader := dpopInfo.headers[0]

cnf, ok := accessToken.Get("cnf")
if !ok {
return fmt.Errorf("missing `cnf` claim in access token")
}

cnfDict, ok := cnf.(map[string]interface{})
if !ok {
return fmt.Errorf("got `cnf` in an invalid format")
}

jktI, ok := cnfDict["jkt"]
if !ok {
return fmt.Errorf("missing `jkt` field in `cnf` claim. only thumbprint JWK confirmation is supported")
}

jkt, ok := jktI.(string)
if !ok {
return fmt.Errorf("invalid `jkt` field in `cnf` claim: %v. the value must be a JWK thumbprint", jkt)
}

dpop, err := jws.Parse([]byte(dpopHeader))
if err != nil {
slog.Error("error parsing JWT: %w", err)
return fmt.Errorf("invalid DPoP JWT")
}
if len(dpop.Signatures()) != 1 {
return fmt.Errorf("expected one signature on DPoP JWT, got %d", len(dpop.Signatures()))
}
sig := dpop.Signatures()[0]
protectedHeaders := sig.ProtectedHeaders()
if protectedHeaders.Type() != "dpop+jwt" {
return fmt.Errorf("invalid typ on DPoP JWT: %v", protectedHeaders.Type())
}

if _, exists := allowedSignatureAlgorithms[protectedHeaders.Algorithm()]; !exists {
return fmt.Errorf("unsupported algorithm specified: %v", protectedHeaders.Algorithm())
}

dpopKey := protectedHeaders.JWK()
if dpopKey == nil {
return fmt.Errorf("JWK missing in DPoP JWT")
}

isPrivate, err := jwk.IsPrivateKey(dpopKey)
if err != nil {
slog.Error("error checking if key is private", err)
return fmt.Errorf("invalid DPoP key specified")
}

if isPrivate {
return fmt.Errorf("cannot use a private key for DPoP")
}

thumbprint, err := dpopKey.Thumbprint(crypto.SHA256)
if err != nil {
slog.Error("error computing thumbprint for key", err)
return fmt.Errorf("couldn't compute thumbprint for key in `jwk` in DPoP JWT")
}

if base64.URLEncoding.EncodeToString(thumbprint) != jkt {
return fmt.Errorf("the `jkt` from the DPoP JWT didn't match the thumbprint from the access token")
}

// at this point we have the right key because its thumbprint matches the `jkt` claim
// in the validated access token
dpopToken, err := jwt.Parse([]byte(dpopHeader), jwt.WithKey(protectedHeaders.Algorithm(), dpopKey))

if err != nil {
slog.Error("error validating DPoP JWT", err)
return fmt.Errorf("failed to verify signature on DPoP JWT")
}

issuedAt := dpopToken.IssuedAt()
if issuedAt.IsZero() {
return fmt.Errorf("missing `iat` claim in the DPoP JWT")
}

if issuedAt.Add(time.Minute * 60).Before(time.Now()) {
return fmt.Errorf("the DPoP JWT has expired")
}

htm, ok := dpopToken.Get("htm")
if !ok {
return fmt.Errorf("`htm` claim missing in DPoP JWT")
}

if htm != dpopInfo.method {
return fmt.Errorf("incorrect `htm` claim in DPoP JWT")
}

htu, ok := dpopToken.Get("htu")
if !ok {
return fmt.Errorf("`htu` claim missing in DPoP JWT")
}

if htu != dpopInfo.path {
return fmt.Errorf("incorrect `htu` claim in DPoP JWT")
}

ath, ok := dpopToken.Get("ath")
if !ok {
return fmt.Errorf("missing `ath` claim in DPoP JWT")
}

h := sha256.New()
h.Write([]byte(acessTokenRaw))
if ath != base64.URLEncoding.EncodeToString(h.Sum(nil)) {
return fmt.Errorf("incorrect `ath` claim in DPoP JWT")
}

return nil
}

Expand Down
Loading