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 app_tid #94

Merged
merged 15 commits into from
Oct 16, 2023
18 changes: 9 additions & 9 deletions auth/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,26 +210,26 @@ func TestEnd2End(t *testing.T) {
claims: oidcMockServer.DefaultClaims(),
wantErr: true,
}, {
name: "jwks rejects zone",
name: "jwks prioritize app_tid",
header: oidcMockServer.DefaultHeaders(),
claims: mocks.NewOIDCClaimsBuilder(oidcMockServer.DefaultClaims()).
ZoneID(mocks.InvalidZoneID).
ZoneID(mocks.InvalidAppTID).
Build(),
wantErr: true,
wantErr: false,
}, {
name: "lib rejects unaccepted zone again",
name: "lib accepts any app_tid",
header: oidcMockServer.DefaultHeaders(),
claims: mocks.NewOIDCClaimsBuilder(oidcMockServer.DefaultClaims()).
ZoneID(mocks.InvalidZoneID).
AppTID(uuid.New().String()).
Build(),
wantErr: true,
wantErr: false,
}, {
name: "lib accepts any zone",
name: "lib rejects unaccepted app_tid",
header: oidcMockServer.DefaultHeaders(),
claims: mocks.NewOIDCClaimsBuilder(oidcMockServer.DefaultClaims()).
ZoneID(uuid.New().String()).
AppTID(mocks.InvalidAppTID).
Build(),
wantErr: false,
wantErr: true,
},
}

Expand Down
25 changes: 22 additions & 3 deletions auth/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ const (
claimEmail = "email"
claimSapGlobalUserID = "user_uuid"
claimSapGlobalZoneID = "zone_uuid" // tenant GUID
claimSapGlobalAppTID = "app_tid"
claimIasIssuer = "ias_iss"
claimAzp = "azp"
)

type Token struct {
Expand Down Expand Up @@ -115,10 +117,27 @@ func (t Token) Email() string {
return v
}

// ZoneID returns "zone_uuid" claim, if it doesn't exist empty string is returned
// ZoneID returns "app_tid" claim, if it doesn't exist empty string is returned
// Deprecated: is replaced by AppTID and will be removed with the next major release
func (t Token) ZoneID() string {
v, _ := t.GetClaimAsString(claimSapGlobalZoneID)
return v
appTID := t.AppTID()
if appTID == "" {
zoneUUID, _ := t.GetClaimAsString(claimSapGlobalZoneID)
return zoneUUID
}
return appTID
}

// AppTID returns "app_tid" claim, if it doesn't exist empty string is returned
func (t Token) AppTID() string {
appTID, _ := t.GetClaimAsString(claimSapGlobalAppTID)
return appTID
}

// Azp returns "azp" claim, if it doesn't exist empty string is returned
func (t Token) Azp() string {
appTID, _ := t.GetClaimAsString(claimAzp)
return appTID
}

// UserUUID returns "user_uuid" claim, if it doesn't exist empty string is returned
Expand Down
7 changes: 6 additions & 1 deletion auth/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ func (m *Middleware) verifySignature(t Token, keySet *oidcclient.OIDCTenant) (er
}

// parse and verify signature
jwks, err := keySet.GetJWKs(t.ZoneID())
tenantOpts := oidcclient.ClientInfo{
ClientID: m.identity.GetClientID(),
AppTID: t.AppTID(),
Azp: t.Azp(),
}
jwks, err := keySet.GetJWKs(tenantOpts)
if err != nil {
return err
}
Expand Down
19 changes: 17 additions & 2 deletions env/iasConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ type Identity interface {
GetClientSecret() string // Returns the client secret. Optional
GetURL() string // Returns the url to the DefaultIdentity tenant. E.g. https://abcdefgh.accounts.ondemand.com
GetDomains() []string // Returns the domains of the DefaultIdentity service. E.g. ["accounts.ondemand.com"]
GetZoneUUID() uuid.UUID // Returns the zone uuid. Optional
GetZoneUUID() uuid.UUID // Deprecated: Returns the zone uuid, will be replaced by GetAppTID Optional
GetAppTID() string // Returns the app tid uuid and replaces zone uuid in future Optional
GetProofTokenURL() string // Returns the proof token url. Optional
GetCertificate() string // Returns the client certificate. Optional
GetKey() string // Returns the client certificate key. Optional
Expand All @@ -47,7 +48,8 @@ type DefaultIdentity struct {
ClientSecret string `json:"clientsecret"`
Domains []string `json:"domains"`
URL string `json:"url"`
ZoneUUID uuid.UUID `json:"zone_uuid"`
ZoneUUID uuid.UUID `json:"zone_uuid"` // Deprecated: will be replaced by AppTID
AppTID string `json:"app_tid"` // replaces ZoneUUID
ProofTokenURL string `json:"prooftoken_url"`
OsbURL string `json:"osb_url"`
Certificate string `json:"certificate"`
Expand Down Expand Up @@ -184,10 +186,23 @@ func (c DefaultIdentity) GetDomains() []string {
}

// GetZoneUUID implements the env.Identity interface.
// Deprecated: is replaced by GetAppTID and will be removed with the next major release
func (c DefaultIdentity) GetZoneUUID() uuid.UUID {
appTid, err := uuid.Parse(c.AppTID)
if err != nil {
return appTid
}
return c.ZoneUUID
}

// GetAppTID implements the env.Identity interface and replaces GetZoneUUID in future
func (c DefaultIdentity) GetAppTID() string {
if c.AppTID != "" {
return c.AppTID
}
return c.ZoneUUID.String()
}

// GetProofTokenURL implements the env.Identity interface.
func (c DefaultIdentity) GetProofTokenURL() string {
return c.ProofTokenURL
Expand Down
3 changes: 2 additions & 1 deletion env/iasConfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var testConfig = &DefaultIdentity{
Domains: []string{"accounts400.ondemand.com", "my.arbitrary.domain"},
URL: "https://mytenant.accounts400.ondemand.com",
ZoneUUID: uuid.MustParse("bef12345-de57-480f-be92-1d8c1c7abf16"),
AppTID: "70cd0de3-528a-4655-b56a-5862591def5c",
}

func TestParseIdentityConfig(t *testing.T) {
Expand All @@ -33,7 +34,7 @@ func TestParseIdentityConfig(t *testing.T) {
}{
{
name: "[CF] single identity service instance bound",
env: "{\"identity\":[{\"binding_name\":null,\"credentials\":{\"clientid\":\"cef76757-de57-480f-be92-1d8c1c7abf16\",\"clientsecret\":\"[the_CLIENT.secret:3[/abc\",\"domains\":[\"accounts400.ondemand.com\",\"my.arbitrary.domain\"],\"token_url\":\"https://mytenant.accounts400.ondemand.com/oauth2/token\",\"url\":\"https://mytenant.accounts400.ondemand.com\",\"zone_uuid\":\"bef12345-de57-480f-be92-1d8c1c7abf16\"},\"instance_name\":\"my-ams-instance\",\"label\":\"identity\",\"name\":\"my-ams-instance\",\"plan\":\"application\",\"provider\":null,\"syslog_drain_url\":null,\"tags\":[\"ias\"],\"volume_mounts\":[]}]}",
env: "{\"identity\":[{\"binding_name\":null,\"credentials\":{\"clientid\":\"cef76757-de57-480f-be92-1d8c1c7abf16\",\"clientsecret\":\"[the_CLIENT.secret:3[/abc\",\"domains\":[\"accounts400.ondemand.com\",\"my.arbitrary.domain\"],\"token_url\":\"https://mytenant.accounts400.ondemand.com/oauth2/token\",\"url\":\"https://mytenant.accounts400.ondemand.com\",\"zone_uuid\":\"bef12345-de57-480f-be92-1d8c1c7abf16\", \"app_tid\":\"70cd0de3-528a-4655-b56a-5862591def5c\"},\"instance_name\":\"my-ams-instance\",\"label\":\"identity\",\"name\":\"my-ams-instance\",\"plan\":\"application\",\"provider\":null,\"syslog_drain_url\":null,\"tags\":[\"ias\"],\"volume_mounts\":[]}]}",
want: testConfig,
wantErr: false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"accounts400.ondemand.com", "my.arbitrary.domain"
],
"url": "https://mytenant.accounts400.ondemand.com",
"zone_uuid": "bef12345-de57-480f-be92-1d8c1c7abf16"
"zone_uuid": "bef12345-de57-480f-be92-1d8c1c7abf16",
"app_tid": "70cd0de3-528a-4655-b56a-5862591def5c"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
70cd0de3-528a-4655-b56a-5862591def5c
20 changes: 13 additions & 7 deletions mocks/mockServer.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ type MockServer struct {
CustomIssuer string // CustomIssuer holds a custom domain returned by the discovery endpoint
}

// InvalidZoneID represents a zone guid which is rejected by mock server on behalf of IAS tenant
const InvalidZoneID string = "dff69954-a259-4104-9074-193bc9a366ce"
// InvalidAppTID represents a guid which is rejected by mock server on behalf of IAS tenant
const InvalidAppTID string = "dff69954-a259-4104-9074-193bc9a366ce"

// NewOIDCMockServer instantiates a new MockServer.
func NewOIDCMockServer() (*MockServer, error) {
Expand Down Expand Up @@ -93,7 +93,7 @@ func newOIDCMockServer(customIssuer string) (*MockServer, error) {
}

r.HandleFunc("/.well-known/openid-configuration", mockServer.WellKnownHandler).Methods(http.MethodGet)
r.HandleFunc("/oauth2/certs", mockServer.JWKsHandlerInvalidZone).Methods(http.MethodGet).Headers("x-zone_uuid", InvalidZoneID)
r.HandleFunc("/oauth2/certs", mockServer.JWKsHandlerInvalidAppTID).Methods(http.MethodGet).Headers("x-app_tid", InvalidAppTID)
r.HandleFunc("/oauth2/certs", mockServer.JWKsHandler).Methods(http.MethodGet)
r.HandleFunc("/oauth2/token", mockServer.tokenHandler).Methods(http.MethodPost).Headers("Content-Type", "application/x-www-form-urlencoded")

Expand Down Expand Up @@ -150,9 +150,9 @@ func (m *MockServer) JWKsHandler(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write(payload)
}

// JWKsHandlerInvalidZone is the http handler which answers invalid requests to the JWKS endpoint.
// in reality it returns "{ \"msg\":\"Invalid zone_uuid provided\" }"
func (m *MockServer) JWKsHandlerInvalidZone(w http.ResponseWriter, _ *http.Request) {
// JWKsHandlerInvalidAppTID is the http handler which answers invalid requests to the JWKS endpoint.
// in reality, it returns "{ \"msg\":\"Invalid app_tid provided\" }"
func (m *MockServer) JWKsHandlerInvalidAppTID(w http.ResponseWriter, _ *http.Request) {
m.JWKsHitCounter++
w.WriteHeader(http.StatusBadRequest)
}
Expand Down Expand Up @@ -271,7 +271,7 @@ func (m *MockServer) DefaultClaims() OIDCClaims {
GivenName: "Foo",
FamilyName: "Bar",
Email: "foo@bar.org",
ZoneID: "11111111-2222-3333-4444-888888888888",
AppTID: "11111111-2222-3333-4444-888888888888",
UserUUID: "22222222-3333-4444-5555-666666666666",
}
return claims
Expand All @@ -295,6 +295,7 @@ type MockConfig struct {
URL string
Domains []string
ZoneUUID uuid.UUID
AppTID string
ProofTokenURL string
OsbURL string
Certificate string
Expand Down Expand Up @@ -327,6 +328,11 @@ func (c MockConfig) GetZoneUUID() uuid.UUID {
return c.ZoneUUID
}

// GetAppTID implements the env.Identity interface.
func (c MockConfig) GetAppTID() string {
return c.AppTID
}

// GetProofTokenURL implements the env.Identity interface.
func (c MockConfig) GetProofTokenURL() string {
return c.ProofTokenURL
Expand Down
1 change: 1 addition & 0 deletions mocks/oidcClaims.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ type OIDCClaims struct {
FamilyName string `json:"family_name,omitempty"`
Email string `json:"email,omitempty"`
ZoneID string `json:"zone_uuid,omitempty"`
AppTID string `json:"app_tid,omitempty"`
UserUUID string `json:"user_uuid,omitempty"`
}
6 changes: 6 additions & 0 deletions mocks/oidcTokenBuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ func (b *OIDCClaimsBuilder) ZoneID(zoneID string) *OIDCClaimsBuilder {
return b
}

// AppTID sets the app_tid field
func (b *OIDCClaimsBuilder) AppTID(appTID string) *OIDCClaimsBuilder {
b.claims.AppTID = appTID
return b
}

// WithoutAudience removes the aud claim
func (b *OIDCClaimsBuilder) WithoutAudience() *OIDCClaimsBuilder {
b.claims.Audience = nil
Expand Down
62 changes: 41 additions & 21 deletions oidcclient/jwk.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,27 @@ import (
)

const defaultJwkExpiration = 15 * time.Minute
const zoneIDHeader = "x-zone_uuid"
const appTIDHeader = "x-app_tid"
const clientIDHeader = "x-client_id"
const azpHeader = "x-azp"

// OIDCTenant represents one IAS tenant correlating with one zone with it's OIDC discovery results and cached JWKs
// OIDCTenant represents one IAS tenant correlating with one app_tid and client_id with it's OIDC discovery results and cached JWKs
type OIDCTenant struct {
ProviderJSON ProviderJSON
acceptedZoneIds map[string]bool
acceptedClients map[ClientInfo]bool
httpClient *http.Client
// A set of cached keys and their expiry.
jwks jwk.Set
jwksExpiry time.Time
mu sync.RWMutex
}

type ClientInfo struct {
ClientID string
AppTID string
Azp string
}

type updateKeysResult struct {
keys jwk.Set
expiry time.Time
Expand All @@ -43,7 +51,7 @@ type updateKeysResult struct {
func NewOIDCTenant(httpClient *http.Client, targetIss *url.URL) (*OIDCTenant, error) {
ks := new(OIDCTenant)
ks.httpClient = httpClient
ks.acceptedZoneIds = make(map[string]bool)
ks.acceptedClients = make(map[ClientInfo]bool)
err := ks.performDiscovery(targetIss.Host)
if err != nil {
return nil, err
Expand All @@ -53,39 +61,39 @@ func NewOIDCTenant(httpClient *http.Client, targetIss *url.URL) (*OIDCTenant, er
}

// GetJWKs returns the validation keys either cached or updated ones
func (ks *OIDCTenant) GetJWKs(zoneID string) (jwk.Set, error) {
keys, err := ks.readJWKsFromMemory(zoneID)
func (ks *OIDCTenant) GetJWKs(clientInfo ClientInfo) (jwk.Set, error) {
keys, err := ks.readJWKsFromMemory(clientInfo)
if keys == nil {
if err != nil {
return nil, err
}
return ks.updateJWKsMemory(zoneID)
return ks.updateJWKsMemory(clientInfo)
}
return keys, nil
}

// readJWKsFromMemory returns the validation keys from memory, or error in case of invalid zone or nil, in case nothing found in memory
func (ks *OIDCTenant) readJWKsFromMemory(zoneID string) (jwk.Set, error) {
// readJWKsFromMemory returns the validation keys from memory, or error in case of invalid header combination or nil, in case nothing found in memory
func (ks *OIDCTenant) readJWKsFromMemory(clientInfo ClientInfo) (jwk.Set, error) {
ks.mu.RLock()
defer ks.mu.RUnlock()

isZoneAccepted, isZoneKnown := ks.acceptedZoneIds[zoneID]
isClientAccepted, isClientKnown := ks.acceptedClients[clientInfo]

if time.Now().Before(ks.jwksExpiry) && isZoneKnown {
if isZoneAccepted {
if time.Now().Before(ks.jwksExpiry) && isClientKnown {
if isClientAccepted {
return ks.jwks, nil
}
return nil, fmt.Errorf("zone_uuid %v is not accepted by the identity service tenant", zoneID)
return nil, fmt.Errorf("client credentials: %+v are not accepted by the identity service", clientInfo)
}
return nil, nil
}

// updateJWKsMemory updates and returns the validation keys from memory, or error in case of invalid zone or nil, in case nothing found in memory
func (ks *OIDCTenant) updateJWKsMemory(zoneID string) (jwk.Set, error) {
// updateJWKsMemory updates and returns the validation keys from memory, or error in case of invalid header combination nil, in case nothing found in memory
func (ks *OIDCTenant) updateJWKsMemory(clientInfo ClientInfo) (jwk.Set, error) {
ks.mu.Lock()
defer ks.mu.Unlock()

updatedKeys, err := ks.getJWKsFromServer(zoneID)
updatedKeys, err := ks.getJWKsFromServer(clientInfo)
if err != nil {
return nil, fmt.Errorf("error updating JWKs: %v", err)
}
Expand All @@ -96,13 +104,16 @@ func (ks *OIDCTenant) updateJWKsMemory(zoneID string) (jwk.Set, error) {
return ks.jwks, nil
}

func (ks *OIDCTenant) getJWKsFromServer(zoneID string) (r interface{}, err error) {
func (ks *OIDCTenant) getJWKsFromServer(clientInfo ClientInfo) (r interface{}, err error) {
result := updateKeysResult{}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, ks.ProviderJSON.JWKsURL, http.NoBody)
if err != nil {
return result, fmt.Errorf("can't create request to fetch jwk: %v", err)
}
req.Header.Add(zoneIDHeader, zoneID)
// at least client-id is necessary, all further headers only refine the validation
req.Header.Add(clientIDHeader, clientInfo.ClientID)
req.Header.Add(appTIDHeader, clientInfo.AppTID)
req.Header.Add(azpHeader, clientInfo.Azp)

resp, err := ks.httpClient.Do(req)
if err != nil {
Expand All @@ -111,10 +122,19 @@ func (ks *OIDCTenant) getJWKsFromServer(zoneID string) (r interface{}, err error
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
ks.acceptedZoneIds[zoneID] = false
return result, fmt.Errorf("failed to fetch jwks from remote for x-zone_uuid %s: %v (%s)", zoneID, err, resp.Body)
// prevent caching ias backend flaps like 503 -> only cache 400
if resp.StatusCode == http.StatusBadRequest {
ks.acceptedClients[clientInfo] = false
}
resp, err := io.ReadAll(resp.Body)
if err != nil {
return result, fmt.Errorf(
"failed to fetch jwks from remote for client credentials %+v: %v", clientInfo, err)
}
return result, fmt.Errorf(
"failed to fetch jwks from remote for client credentials %+v: (%s)", clientInfo, resp)
}
ks.acceptedZoneIds[zoneID] = true
ks.acceptedClients[clientInfo] = true
jwks, err := jwk.ParseReader(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to parse JWK set: %w", err)
Expand Down
Loading
Loading