Skip to content

Commit

Permalink
Add new method NewTokenFromClaims (#60)
Browse files Browse the repository at this point in the history
* Add new method NewTokenFromClaims

* Move token constructors to mocks package

* Move token constructors to mocks package

* Sort imports

* Remove import cycle in test

* Refactor internal usage of stdToken

* Add license

* Move NewTokenFromClaims to separate package

* Rename var

Co-authored-by: Nena Raab <nena.raab@sap.com>
  • Loading branch information
f-blass and nenaraab authored Mar 9, 2022
1 parent 33d1cee commit 3c60a76
Show file tree
Hide file tree
Showing 19 changed files with 185 additions and 121 deletions.
7 changes: 4 additions & 3 deletions auth/certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
_ "embed"
"encoding/base64"
"encoding/pem"
"math/big"
"testing"

"github.com/lestrrat-go/jwx/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"math/big"
"testing"
)

//go:embed testdata/x-forwarded-client-cert.txt
Expand Down Expand Up @@ -59,7 +60,7 @@ func generateToken(t *testing.T, claimCnfMemberX5tValue string) Token {
err := token.Set(claimCnf, cnfClaim)
require.NoError(t, err, "Failed to create token: %v", err)

return stdToken{jwtToken: token}
return Token{jwtToken: token}
}

func convertToPEM(t *testing.T, derCert string) string {
Expand Down
15 changes: 8 additions & 7 deletions auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ package auth

import (
"context"
"github.com/sap/cloud-security-client-go/env"
"github.com/sap/cloud-security-client-go/httpclient"
"github.com/sap/cloud-security-client-go/tokenclient"
"log"
"net/http"
"time"

"github.com/patrickmn/go-cache"
"golang.org/x/sync/singleflight"

"github.com/sap/cloud-security-client-go/env"
"github.com/sap/cloud-security-client-go/httpclient"
"github.com/sap/cloud-security-client-go/tokenclient"
)

// The ContextKey type is used as a key for library related values in the go context. See also TokenCtxKey
Expand Down Expand Up @@ -111,24 +112,24 @@ func (m *Middleware) AuthenticateWithProofOfPossession(r *http.Request) (Token,
// get Token from Header
rawToken, err := extractRawToken(r)
if err != nil {
return nil, nil, err
return Token{}, nil, err
}

token, err := m.parseAndValidateJWT(rawToken)
if err != nil {
return nil, nil, err
return Token{}, nil, err
}

const forwardedClientCertHeader = "x-forwarded-client-cert"
var cert *Certificate
cert, err = newCertificate(r.Header.Get(forwardedClientCertHeader))
if err != nil {
return nil, nil, err
return Token{}, nil, err
}
if "1" == "" && cert != nil { // TODO integrate proof of possession into middleware
err = validateCertificate(cert, token)
if err != nil {
return nil, nil, err
return Token{}, nil, err
}
}

Expand Down
4 changes: 2 additions & 2 deletions auth/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ package auth

import (
"context"
"github.com/sap/cloud-security-client-go/env"
"github.com/stretchr/testify/assert"
"io"
"net/http"
"net/http/httptest"
Expand All @@ -17,7 +15,9 @@ import (

"github.com/google/uuid"
"github.com/lestrrat-go/jwx/jwa"
"github.com/stretchr/testify/assert"

"github.com/sap/cloud-security-client-go/env"
"github.com/sap/cloud-security-client-go/mocks"
)

Expand Down
7 changes: 0 additions & 7 deletions auth/proofOfPossession.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@ import (
)

var ErrNoClientCert = errors.New("there is no x509 client certificate provided")
var ErrNoToken = errors.New("there is no token provided")

// validateCertificate runs all proof of possession checks.
// This ensures that the token was issued for the sender.
func validateCertificate(clientCertificate *Certificate, token Token) error {
if clientCertificate == nil {
return ErrNoClientCert
}
if token == nil {
return ErrNoToken
}
return validateX5tThumbprint(clientCertificate, token)
}

Expand All @@ -33,9 +29,6 @@ func validateX5tThumbprint(clientCertificate *Certificate, token Token) error {
if clientCertificate == nil {
return ErrNoClientCert
}
if token == nil {
return ErrNoToken
}

cnfThumbprint := token.getCnfClaimMember(claimCnfMemberX5t)
if cnfThumbprint == "" {
Expand Down
17 changes: 2 additions & 15 deletions auth/proofOfPossession_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
package auth

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)

var derCertGenerated = generateDERCert()
Expand All @@ -17,13 +18,6 @@ func TestProofOfPossession_parseAndValidateCertificate_edgeCases(t *testing.T) {
assert.Equal(t, "there is no x509 client certificate provided", err.Error())
})

t.Run("validateCertificate() fails when no token is given", func(t *testing.T) {
x509Cert, err := newCertificate(derCertGenerated)
require.NoError(t, err, "Failed to parse cert header: %v", err)
err = validateX5tThumbprint(x509Cert, nil)
assert.Equal(t, "there is no token provided", err.Error())
})

t.Run("validateCertificate() fails when cert does not match x5t", func(t *testing.T) {
x509Cert, err := newCertificate(derCertGenerated)
require.NoError(t, err, "Failed to parse cert header: %v", err)
Expand All @@ -37,13 +31,6 @@ func TestProofOfPossession_validateX5tThumbprint_edgeCases(t *testing.T) {
err := validateX5tThumbprint(nil, generateToken(t, "abc"))
assert.Equal(t, "there is no x509 client certificate provided", err.Error())
})

t.Run("validateX5tThumbprint() fails when no token is given", func(t *testing.T) {
x509Cert, err := newCertificate(derCertGenerated)
require.NoError(t, err, "Failed to parse cert header: %v", err)
err = validateX5tThumbprint(x509Cert, nil)
assert.Equal(t, "there is no token provided", err.Error())
})
}

func TestProofOfPossession_validateX5tThumbprint(t *testing.T) {
Expand Down
91 changes: 42 additions & 49 deletions auth/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,7 @@ const (
claimIasIssuer = "ias_iss"
)

// Token is the public API to access claims of the token
type Token interface {
TokenValue() string // TokenValue returns encoded token string
Audience() []string // Audience returns "aud" claim, if it doesn't exist empty string is returned
Expiration() time.Time // Expiration returns "exp" claim, if it doesn't exist empty string is returned
IsExpired() bool // IsExpired returns true, if 'exp' claim + leeway time of 1 minute is before current time
IssuedAt() time.Time // IssuedAt returns "iat" claim, if it doesn't exist empty string is returned
CustomIssuer() string // CustomIssuer returns "iss" claim if it is a custom domain (i.e. "ias_iss" claim available), otherwise empty string is returned
Issuer() string // Issuer returns token issuer with SAP domain; by default "iss" claim is returned or in case it is a custom domain, "ias_iss" is returned
NotBefore() time.Time // NotBefore returns "nbf" claim, if it doesn't exist empty string is returned
Subject() string // Subject returns "sub" claim, if it doesn't exist empty string is returned
GivenName() string // GivenName returns "given_name" claim, if it doesn't exist empty string is returned
FamilyName() string // FamilyName returns "family_name" claim, if it doesn't exist empty string is returned
Email() string // Email returns "email" claim, if it doesn't exist empty string is returned
ZoneID() string // ZoneID returns "zone_uuid" claim, if it doesn't exist empty string is returned
UserUUID() string // UserUUID returns "user_uuid" claim, if it doesn't exist empty string is returned
HasClaim(claim string) bool // HasClaim returns true if the provided claim exists in the token
GetClaimAsString(claim string) (string, error) // GetClaimAsString returns a custom claim type asserted as string. Returns error if the claim is not available or not a string.
GetClaimAsStringSlice(claim string) ([]string, error) // GetClaimAsStringSlice returns a custom claim type asserted as string slice. The claim name is case sensitive. Returns error if the claim is not available or not an array
GetClaimAsMap(claim string) (map[string]interface{}, error) // GetClaimAsMap returns a map of all members and its values of a custom claim in the token. The member name is case sensitive. Returns error if the claim is not available or not a map
GetAllClaimsAsMap() map[string]interface{} // GetAllClaimsAsMap returns a map of all claims contained in the token. The claim name is case sensitive. Includes also custom claims
getJwtToken() jwt.Token
getCnfClaimMember(memberName string) string // getCnfClaimMember returns "cnf" claim. The cnf member name is case sensitive. If it doesn't exist empty string is returned
}

type stdToken struct {
type Token struct {
encodedToken string
jwtToken jwt.Token
}
Expand All @@ -59,45 +34,51 @@ type stdToken struct {
func NewToken(encodedToken string) (Token, error) {
decodedToken, err := jwt.ParseString(encodedToken, jwt.WithToken(openid.New()))
if err != nil {
return nil, err
return Token{}, err
}

return stdToken{
return Token{
encodedToken: encodedToken,
jwtToken: decodedToken, // encapsulates jwt.token_gen from github.com/lestrrat-go/jwx/jwt
}, nil
}

// TokenValue returns encoded token string
func (t stdToken) TokenValue() string {
func (t Token) TokenValue() string {
return t.encodedToken
}

func (t stdToken) Audience() []string {
// Audience returns "aud" claim, if it doesn't exist empty string is returned
func (t Token) Audience() []string {
return t.jwtToken.Audience()
}

func (t stdToken) Expiration() time.Time {
// Expiration returns "exp" claim, if it doesn't exist empty string is returned
func (t Token) Expiration() time.Time {
return t.jwtToken.Expiration()
}

func (t stdToken) IsExpired() bool {
// IsExpired returns true, if 'exp' claim + leeway time of 1 minute is before current time
func (t Token) IsExpired() bool {
return t.Expiration().Add(1 * time.Minute).Before(time.Now())
}

func (t stdToken) IssuedAt() time.Time {
// IssuedAt returns "iat" claim, if it doesn't exist empty string is returned
func (t Token) IssuedAt() time.Time {
return t.jwtToken.IssuedAt()
}

func (t stdToken) CustomIssuer() string {
// CustomIssuer returns "iss" claim if it is a custom domain (i.e. "ias_iss" claim available), otherwise empty string is returned
func (t Token) CustomIssuer() string {
// only return iss if ias_iss does exist
if !t.HasClaim(claimIasIssuer) {
return ""
}
return t.jwtToken.Issuer()
}

func (t stdToken) Issuer() string {
// Issuer returns token issuer with SAP domain; by default "iss" claim is returned or in case it is a custom domain, "ias_iss" is returned
func (t Token) Issuer() string {
// return standard issuer if ias_iss is not set
v, err := t.GetClaimAsString(claimIasIssuer)
if errors.Is(err, ErrClaimNotExists) {
Expand All @@ -106,48 +87,57 @@ func (t stdToken) Issuer() string {
return v
}

func (t stdToken) NotBefore() time.Time {
// NotBefore returns "nbf" claim, if it doesn't exist empty string is returned
func (t Token) NotBefore() time.Time {
return t.jwtToken.NotBefore()
}

func (t stdToken) Subject() string {
// Subject returns "sub" claim, if it doesn't exist empty string is returned
func (t Token) Subject() string {
return t.jwtToken.Subject()
}

func (t stdToken) GivenName() string {
// GivenName returns "given_name" claim, if it doesn't exist empty string is returned
func (t Token) GivenName() string {
v, _ := t.GetClaimAsString(claimGivenName)
return v
}

func (t stdToken) FamilyName() string {
// FamilyName returns "family_name" claim, if it doesn't exist empty string is returned
func (t Token) FamilyName() string {
v, _ := t.GetClaimAsString(claimFamilyName)
return v
}

func (t stdToken) Email() string {
// Email returns "email" claim, if it doesn't exist empty string is returned
func (t Token) Email() string {
v, _ := t.GetClaimAsString(claimEmail)
return v
}

func (t stdToken) ZoneID() string {
// ZoneID returns "zone_uuid" claim, if it doesn't exist empty string is returned
func (t Token) ZoneID() string {
v, _ := t.GetClaimAsString(claimSapGlobalZoneID)
return v
}

func (t stdToken) UserUUID() string {
// UserUUID returns "user_uuid" claim, if it doesn't exist empty string is returned
func (t Token) UserUUID() string {
v, _ := t.GetClaimAsString(claimSapGlobalUserID)
return v
}

// ErrClaimNotExists shows that the requested custom claim does not exist in the token
var ErrClaimNotExists = errors.New("claim does not exist in the token")

func (t stdToken) HasClaim(claim string) bool {
// HasClaim returns true if the provided claim exists in the token
func (t Token) HasClaim(claim string) bool {
_, exists := t.jwtToken.Get(claim)
return exists
}

func (t stdToken) GetClaimAsString(claim string) (string, error) {
// GetClaimAsString returns a custom claim type asserted as string. Returns error if the claim is not available or not a string.
func (t Token) GetClaimAsString(claim string) (string, error) {
value, exists := t.jwtToken.Get(claim)
if !exists {
return "", ErrClaimNotExists
Expand All @@ -159,7 +149,8 @@ func (t stdToken) GetClaimAsString(claim string) (string, error) {
return stringValue, nil
}

func (t stdToken) GetClaimAsStringSlice(claim string) ([]string, error) {
// GetClaimAsStringSlice returns a custom claim type asserted as string slice. The claim name is case sensitive. Returns error if the claim is not available or not an array
func (t Token) GetClaimAsStringSlice(claim string) ([]string, error) {
value, exists := t.jwtToken.Get(claim)
if !exists {
return nil, ErrClaimNotExists
Expand All @@ -171,12 +162,14 @@ func (t stdToken) GetClaimAsStringSlice(claim string) ([]string, error) {
return res, nil
}

func (t stdToken) GetAllClaimsAsMap() map[string]interface{} {
// GetAllClaimsAsMap returns a map of all claims contained in the token. The claim name is case sensitive. Includes also custom claims
func (t Token) GetAllClaimsAsMap() map[string]interface{} {
mapClaims, _ := t.jwtToken.AsMap(context.TODO()) // err can not really occur on jwt.Token
return mapClaims
}

func (t stdToken) GetClaimAsMap(claim string) (map[string]interface{}, error) {
// GetClaimAsMap returns a map of all members and its values of a custom claim in the token. The member name is case sensitive. Returns error if the claim is not available or not a map
func (t Token) GetClaimAsMap(claim string) (map[string]interface{}, error) {
value, exists := t.jwtToken.Get(claim)
if !exists {
return nil, ErrClaimNotExists
Expand All @@ -188,11 +181,11 @@ func (t stdToken) GetClaimAsMap(claim string) (map[string]interface{}, error) {
return res, nil
}

func (t stdToken) getJwtToken() jwt.Token {
func (t Token) getJwtToken() jwt.Token {
return t.jwtToken
}

func (t stdToken) getCnfClaimMember(memberName string) string {
func (t Token) getCnfClaimMember(memberName string) string {
cnfClaim, err := t.GetClaimAsMap(claimCnf)
if errors.Is(err, ErrClaimNotExists) || cnfClaim == nil {
return ""
Expand Down
Loading

0 comments on commit 3c60a76

Please sign in to comment.