mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-04 20:24:26 +00:00
allow audit correlation between token being issued and being used
This commit is contained in:
committed by
Joshua Casey
parent
c803a182be
commit
4423d472da
@@ -6,25 +6,42 @@ package auditevent
|
||||
type Message string
|
||||
|
||||
const (
|
||||
HTTPRequestReceived Message = "HTTP Request Received"
|
||||
HTTPRequestCompleted Message = "HTTP Request Completed"
|
||||
HTTPRequestParameters Message = "HTTP Request Parameters"
|
||||
HTTPRequestCustomHeadersUsed Message = "HTTP Request Custom Headers Used"
|
||||
UsingUpstreamIDP Message = "Using Upstream IDP"
|
||||
AuthorizeIDFromParameters Message = "AuthorizeID From Parameters"
|
||||
IdentityFromUpstreamIDP Message = "Identity From Upstream IDP"
|
||||
IdentityRefreshedFromUpstreamIDP Message = "Identity Refreshed From Upstream IDP"
|
||||
SessionStarted Message = "Session Started"
|
||||
SessionRefreshed Message = "Session Refreshed"
|
||||
SessionFound Message = "Session Found"
|
||||
AuthenticationRejectedByTransforms Message = "Authentication Rejected By Transforms"
|
||||
UpstreamOIDCTokenRevoked Message = "Upstream OIDC Token Revoked" //nolint:gosec // this is not a credential
|
||||
SessionGarbageCollected Message = "Session Garbage Collected"
|
||||
UpstreamAuthorizeRedirect Message = "Upstream Authorize Redirect"
|
||||
OIDCClientSecretRequestUpdatedSecrets Message = "OIDCClientSecretRequest Updated Secrets"
|
||||
// Supervisor request logging.
|
||||
|
||||
HTTPRequestReceived Message = "HTTP Request Received"
|
||||
HTTPRequestCompleted Message = "HTTP Request Completed"
|
||||
HTTPRequestParameters Message = "HTTP Request Parameters"
|
||||
HTTPRequestCustomHeadersUsed Message = "HTTP Request Custom Headers Used"
|
||||
HTTPRequestBasicAuthUsed Message = "HTTP Request Basic Auth"
|
||||
|
||||
// Supervisor authentication logging.
|
||||
|
||||
UsingUpstreamIDP Message = "Using Upstream IDP"
|
||||
AuthorizeIDFromParameters Message = "AuthorizeID From Parameters"
|
||||
IdentityFromUpstreamIDP Message = "Identity From Upstream IDP"
|
||||
UpstreamAuthorizeRedirect Message = "Upstream Authorize Redirect"
|
||||
IdentityRefreshedFromUpstreamIDP Message = "Identity Refreshed From Upstream IDP"
|
||||
IDTokenIssued Message = "ID Token Issued" //nolint:gosec // this is not a credential
|
||||
SessionStarted Message = "Session Started"
|
||||
SessionRefreshed Message = "Session Refreshed"
|
||||
SessionFound Message = "Session Found"
|
||||
AuthenticationRejectedByTransforms Message = "Authentication Rejected By Transforms"
|
||||
IncorrectUsernameOrPassword Message = "Incorrect Username Or Password"
|
||||
|
||||
// Supervisor session ending logging.
|
||||
|
||||
UpstreamOIDCTokenRevoked Message = "Upstream OIDC Token Revoked" //nolint:gosec // this is not a credential
|
||||
SessionGarbageCollected Message = "Session Garbage Collected"
|
||||
|
||||
// Supervisor aggregated APIs logging.
|
||||
|
||||
OIDCClientSecretRequestUpdatedSecrets Message = "OIDCClientSecretRequest Updated Secrets"
|
||||
|
||||
// Concierge aggregated APIs logging.
|
||||
|
||||
TokenCredentialRequestTokenReceived Message = "TokenCredentialRequest Token Received" //nolint:gosec // this is not a credential
|
||||
TokenCredentialRequestAuthenticatedUser Message = "TokenCredentialRequest Authenticated User" //nolint:gosec // this is not a credential
|
||||
TokenCredentialRequestAuthenticationFailed Message = "TokenCredentialRequest Authentication Failed" //nolint:gosec // this is not a credential
|
||||
TokenCredentialRequestUnexpectedError Message = "TokenCredentialRequest Unexpected Error" //nolint:gosec // this is not a credential
|
||||
TokenCredentialRequestUnsupportedUserInfo Message = "TokenCredentialRequest Unsupported UserInfo" //nolint:gosec // this is not a credential
|
||||
IncorrectUsernameOrPassword Message = "Incorrect Username Or Password" //nolint:gosec // this is not a credential
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -56,6 +57,7 @@ func NewHandler(
|
||||
oauthHelper.WriteAccessError(r.Context(), w, nil, err)
|
||||
return nil
|
||||
}
|
||||
auditLogBasicAuthClientID(r, auditLogger)
|
||||
|
||||
session := psession.NewPinnipedSession()
|
||||
accessRequest, err := oauthHelper.NewAccessRequest(r.Context(), r, session)
|
||||
@@ -114,6 +116,9 @@ func NewHandler(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Allow cross-referencing the token with the Concierge's audit logs.
|
||||
auditLogIDToken(r.Context(), auditLogger, accessRequest, accessResponse)
|
||||
|
||||
oauthHelper.WriteAccessResponse(r.Context(), w, accessRequest, accessResponse)
|
||||
|
||||
return nil
|
||||
@@ -391,3 +396,50 @@ func diffSortedGroups(oldGroups, newGroups []string) ([]string, []string) {
|
||||
removed := oldGroupsAsSet.Difference(newGroupsAsSet) // groups in oldGroups that are not in newGroups i.e. removed
|
||||
return added.List(), removed.List()
|
||||
}
|
||||
|
||||
func auditLogBasicAuthClientID(r *http.Request, auditLogger plog.AuditLogger) {
|
||||
// For dynamic clients, the client ID is from basic auth, not from the request parameters.
|
||||
clientIDFromBasicAuth, _, basicAuthUsed := r.BasicAuth()
|
||||
if basicAuthUsed {
|
||||
auditLogger.Audit(auditevent.HTTPRequestBasicAuthUsed, &plog.AuditParams{
|
||||
ReqCtx: r.Context(),
|
||||
KeysAndValues: []any{"clientID", clientIDFromBasicAuth},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func auditLogIDToken(
|
||||
reqCtx context.Context,
|
||||
auditLogger plog.AuditLogger,
|
||||
accessRequest fosite.AccessRequester,
|
||||
accessResponse fosite.AccessResponder,
|
||||
) {
|
||||
var idToken string
|
||||
|
||||
if accessRequest.GetGrantTypes().ExactOne(oidcapi.GrantTypeTokenExchange) {
|
||||
// Token exchanges return the ID token in the access token field of the response.
|
||||
idToken = accessResponse.GetAccessToken()
|
||||
} else {
|
||||
// For other grant types, there may not be an access token, e.g. when the openid scope was not granted.
|
||||
tok := accessResponse.GetExtra("id_token")
|
||||
if tok != nil {
|
||||
// This should always be a string. Checking just to be safe.
|
||||
tokAsStr, ok := tok.(string)
|
||||
if ok {
|
||||
idToken = tokAsStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(idToken) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
auditLogger.Audit(auditevent.IDTokenIssued, &plog.AuditParams{
|
||||
ReqCtx: reqCtx,
|
||||
Session: accessRequest,
|
||||
KeysAndValues: []any{
|
||||
"tokenIdentifier", fmt.Sprintf("%x", sha256.Sum256([]byte(idToken))),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ type tokenEndpointResponseExpectedValues struct {
|
||||
// The expected lifetime of the ID tokens issued by authcode exchange and refresh, but not token exchange.
|
||||
// When zero, will assume that the test wants the default value for ID token lifetime.
|
||||
wantIDTokenLifetimeSeconds int
|
||||
wantAuditLogs func(sessionID string) []testutil.WantedAuditLog
|
||||
wantAuditLogs func(sessionID string, idToken string) []testutil.WantedAuditLog
|
||||
}
|
||||
|
||||
func withWantCustomIDTokenLifetime(wantIDTokenLifetimeSeconds int, w tokenEndpointResponseExpectedValues) tokenEndpointResponseExpectedValues {
|
||||
@@ -368,6 +368,10 @@ func addDynamicClientIDToFormPostBody(r *http.Request) {
|
||||
r.Form.Set("client_id", dynamicClientID)
|
||||
}
|
||||
|
||||
func idTokenToHash(tok string) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(tok)))
|
||||
}
|
||||
|
||||
func TestTokenEndpointAuthcodeExchange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -387,7 +391,7 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
|
||||
wantGrantedScopes: []string{"openid", "username", "groups"},
|
||||
wantUsername: goodUsername,
|
||||
wantGroups: goodGroups,
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -399,6 +403,10 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
|
||||
},
|
||||
}),
|
||||
testutil.WantAuditLog("Session Found", map[string]any{"sessionID": sessionID}),
|
||||
testutil.WantAuditLog("ID Token Issued", map[string]any{
|
||||
"sessionID": sessionID,
|
||||
"tokenIdentifier": idTokenToHash(idToken),
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -458,7 +466,7 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
|
||||
wantGrantedScopes: []string{"openid", "pinniped:request-audience", "username", "groups"},
|
||||
wantUsername: goodUsername,
|
||||
wantGroups: goodGroups,
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -468,7 +476,12 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
|
||||
"redirect_uri": "http://127.0.0.1/callback",
|
||||
},
|
||||
}),
|
||||
testutil.WantAuditLog("HTTP Request Basic Auth", map[string]any{"clientID": dynamicClientID}),
|
||||
testutil.WantAuditLog("Session Found", map[string]any{"sessionID": sessionID}),
|
||||
testutil.WantAuditLog("ID Token Issued", map[string]any{
|
||||
"sessionID": sessionID,
|
||||
"tokenIdentifier": idTokenToHash(idToken),
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -549,7 +562,7 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
|
||||
wantGrantedScopes: []string{"username", "groups"}, // username and groups were not requested, but granted anyway for backwards compatibility
|
||||
wantUsername: goodUsername,
|
||||
wantGroups: goodGroups,
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -583,6 +596,21 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
|
||||
wantGrantedScopes: []string{"pinniped:request-audience", "username", "groups"},
|
||||
wantUsername: goodUsername,
|
||||
wantGroups: goodGroups,
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
"code": "redacted",
|
||||
"code_verifier": "redacted",
|
||||
"grant_type": "authorization_code",
|
||||
"redirect_uri": "http://127.0.0.1/callback",
|
||||
},
|
||||
}),
|
||||
testutil.WantAuditLog("HTTP Request Basic Auth", map[string]any{"clientID": dynamicClientID}),
|
||||
testutil.WantAuditLog("Session Found", map[string]any{"sessionID": sessionID}),
|
||||
// Note that there was no ID token issued, so there is no "ID Token Issued" audit log.
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -955,7 +983,7 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
|
||||
want: tokenEndpointResponseExpectedValues{
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantErrorResponseBody: fositeMissingPKCEVerifierErrorBody,
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -981,7 +1009,7 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) {
|
||||
want: tokenEndpointResponseExpectedValues{
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantErrorResponseBody: fositeWrongPKCEVerifierErrorBody,
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -1144,7 +1172,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
wantStatus int
|
||||
wantErrorType string
|
||||
wantErrorDescContains string
|
||||
wantAuditLogs func(sessionID string) []testutil.WantedAuditLog
|
||||
wantAuditLogs func(sessionID string, idToken string) []testutil.WantedAuditLog
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
@@ -1188,7 +1216,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
"name": "value",
|
||||
},
|
||||
},
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -1200,13 +1228,17 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
},
|
||||
}),
|
||||
testutil.WantAuditLog("Session Found", map[string]any{"sessionID": sessionID}),
|
||||
testutil.WantAuditLog("ID Token Issued", map[string]any{
|
||||
"sessionID": sessionID,
|
||||
"tokenIdentifier": idTokenToHash(idToken),
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
requestedAudience: "some-workload-cluster",
|
||||
wantStatus: http.StatusOK,
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -1219,6 +1251,10 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
},
|
||||
}),
|
||||
testutil.WantAuditLog("Session Found", map[string]any{"sessionID": sessionID}),
|
||||
testutil.WantAuditLog("ID Token Issued", map[string]any{
|
||||
"sessionID": sessionID,
|
||||
"tokenIdentifier": idTokenToHash(idToken),
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -1394,7 +1430,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantErrorType: "unauthorized_client",
|
||||
wantErrorDescContains: `The client is not authorized to request a token using this method. The OAuth 2.0 Client is not allowed to use token exchange grant 'urn:ietf:params:oauth:grant-type:token-exchange'.`,
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -1405,6 +1441,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
||||
},
|
||||
}),
|
||||
testutil.WantAuditLog("HTTP Request Basic Auth", map[string]any{"clientID": dynamicClientID}),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -1505,7 +1542,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantErrorType: "invalid_request",
|
||||
wantErrorDescContains: "Missing 'audience' parameter.",
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -1784,7 +1821,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
t.Parallel()
|
||||
|
||||
// Authcode exchange doesn't use the upstream provider cache, so just pass an empty cache.
|
||||
subject, rsp, _, _, secrets, oauthStore, actualAuditLog, sessionID := exchangeAuthcodeForTokens(t,
|
||||
subject, rsp, _, _, secrets, oauthStore, actualAuditLog, actualSessionID := exchangeAuthcodeForTokens(t,
|
||||
test.authcodeExchange, testidplister.NewUpstreamIDPListerBuilder().BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources)
|
||||
var parsedAuthcodeExchangeResponseBody map[string]any
|
||||
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody))
|
||||
@@ -1824,12 +1861,6 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
require.Equal(t, test.wantStatus, rsp.Code)
|
||||
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), "application/json")
|
||||
|
||||
if test.wantAuditLogs != nil {
|
||||
wantAuditLogs := test.wantAuditLogs(sessionID)
|
||||
testutil.WantAuditIDOnEveryAuditLog(wantAuditLogs, "fake-token-exchange-audit-id")
|
||||
testutil.CompareAuditLogs(t, wantAuditLogs, actualAuditLog.String())
|
||||
}
|
||||
|
||||
var parsedResponseBody map[string]any
|
||||
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedResponseBody))
|
||||
|
||||
@@ -1845,6 +1876,13 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
require.NotEmpty(t, errorDesc)
|
||||
require.Contains(t, errorDesc, test.wantErrorDescContains)
|
||||
|
||||
// Even in the error case, make assertions about audit logs, but without an ID token.
|
||||
if test.wantAuditLogs != nil {
|
||||
wantAuditLogs := test.wantAuditLogs(actualSessionID, "")
|
||||
testutil.WantAuditIDOnEveryAuditLog(wantAuditLogs, "fake-token-exchange-audit-id")
|
||||
testutil.CompareAuditLogs(t, wantAuditLogs, actualAuditLog.String())
|
||||
}
|
||||
|
||||
// The remaining assertions apply only to the happy path.
|
||||
return
|
||||
}
|
||||
@@ -1860,7 +1898,8 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
require.Equal(t, "urn:ietf:params:oauth:token-type:jwt", parsedResponseBody["issued_token_type"])
|
||||
|
||||
// Parse the returned token.
|
||||
parsedJWT, err := jose.ParseSigned(parsedResponseBody["access_token"].(string), []jose.SignatureAlgorithm{jose.ES256})
|
||||
actualIDToken := parsedResponseBody["access_token"].(string)
|
||||
parsedJWT, err := jose.ParseSigned(actualIDToken, []jose.SignatureAlgorithm{jose.ES256})
|
||||
require.NoError(t, err)
|
||||
var tokenClaims map[string]any
|
||||
require.NoError(t, json.Unmarshal(parsedJWT.UnsafePayloadWithoutVerification(), &tokenClaims))
|
||||
@@ -1948,6 +1987,12 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn
|
||||
newSecrets, err := secrets.List(context.Background(), metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, existingSecrets.Items, newSecrets.Items)
|
||||
|
||||
if test.wantAuditLogs != nil {
|
||||
wantAuditLogs := test.wantAuditLogs(actualSessionID, actualIDToken)
|
||||
testutil.WantAuditIDOnEveryAuditLog(wantAuditLogs, "fake-token-exchange-audit-id")
|
||||
testutil.CompareAuditLogs(t, wantAuditLogs, actualAuditLog.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2184,7 +2229,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
return want
|
||||
}
|
||||
|
||||
refreshResponseWithAuditLogs := func(expectedValues tokenEndpointResponseExpectedValues, wantAuditLogs func(sessionID string) []testutil.WantedAuditLog) tokenEndpointResponseExpectedValues {
|
||||
refreshResponseWithAuditLogs := func(expectedValues tokenEndpointResponseExpectedValues, wantAuditLogs func(sessionID string, idToken string) []testutil.WantedAuditLog) tokenEndpointResponseExpectedValues {
|
||||
expectedValues.wantAuditLogs = wantAuditLogs
|
||||
return expectedValues
|
||||
}
|
||||
@@ -2338,7 +2383,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
||||
refreshedUpstreamTokensWithIDAndRefreshTokens(),
|
||||
),
|
||||
func(sessionID string) []testutil.WantedAuditLog {
|
||||
func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -2367,6 +2412,10 @@ func TestRefreshGrant(t *testing.T) {
|
||||
"subject": "https://issuer?sub=some-subject",
|
||||
},
|
||||
}),
|
||||
testutil.WantAuditLog("ID Token Issued", map[string]any{
|
||||
"sessionID": sessionID,
|
||||
"tokenIdentifier": idTokenToHash(idToken),
|
||||
}),
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -2536,7 +2585,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
"error_description": "Error during upstream refresh. Upstream refresh rejected by configured identity policy: authentication was rejected by a configured policy."
|
||||
}
|
||||
`),
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -2607,7 +2656,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
"name": "value",
|
||||
},
|
||||
},
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -2619,6 +2668,10 @@ func TestRefreshGrant(t *testing.T) {
|
||||
},
|
||||
}),
|
||||
testutil.WantAuditLog("Session Found", map[string]any{"sessionID": sessionID}),
|
||||
testutil.WantAuditLog("ID Token Issued", map[string]any{
|
||||
"sessionID": sessionID,
|
||||
"tokenIdentifier": idTokenToHash(idToken),
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -2973,7 +3026,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
{Text: `User "some-username" has been added to the following groups: ["new-group1" "new-group2" "new-group3"]`},
|
||||
{Text: `User "some-username" has been removed from the following groups: ["group1" "groups2"]`},
|
||||
},
|
||||
wantAuditLogs: func(sessionID string) []testutil.WantedAuditLog {
|
||||
wantAuditLogs: func(sessionID string, idToken string) []testutil.WantedAuditLog {
|
||||
return []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("HTTP Request Parameters", map[string]any{
|
||||
"params": map[string]any{
|
||||
@@ -3007,6 +3060,10 @@ func TestRefreshGrant(t *testing.T) {
|
||||
"subject": "https://issuer?sub=some-subject",
|
||||
},
|
||||
}),
|
||||
testutil.WantAuditLog("ID Token Issued", map[string]any{
|
||||
"sessionID": sessionID,
|
||||
"tokenIdentifier": idTokenToHash(idToken),
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -4955,7 +5012,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
// First exchange the authcode for tokens, including a refresh token.
|
||||
// It's actually fine to use this function even when simulating LDAP (which uses a different flow) because it's
|
||||
// just populating a secret in storage.
|
||||
subject, rsp, authCode, jwtSigningKey, secrets, oauthStore, actualAuditLog, sessionID := exchangeAuthcodeForTokens(t,
|
||||
subject, rsp, authCode, jwtSigningKey, secrets, oauthStore, actualAuditLog, actualSessionID := exchangeAuthcodeForTokens(t,
|
||||
test.authcodeExchange, test.idps.BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources)
|
||||
var parsedAuthcodeExchangeResponseBody map[string]any
|
||||
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody))
|
||||
@@ -4999,12 +5056,6 @@ func TestRefreshGrant(t *testing.T) {
|
||||
t.Logf("second response: %#v", refreshResponse)
|
||||
t.Logf("second response body: %q", refreshResponse.Body.String())
|
||||
|
||||
if test.refreshRequest.want.wantAuditLogs != nil {
|
||||
wantAuditLogs := test.refreshRequest.want.wantAuditLogs(sessionID)
|
||||
testutil.WantAuditIDOnEveryAuditLog(wantAuditLogs, "fake-refresh-grant-audit-id")
|
||||
testutil.CompareAuditLogs(t, wantAuditLogs, actualAuditLog.String())
|
||||
}
|
||||
|
||||
// Test that we did or did not make a call to the upstream provider's interface to perform refresh.
|
||||
switch {
|
||||
case test.refreshRequest.want.wantOIDCUpstreamRefreshCall != nil:
|
||||
@@ -5067,6 +5118,9 @@ func TestRefreshGrant(t *testing.T) {
|
||||
jwtSigningKey,
|
||||
secrets,
|
||||
approxRequestTime,
|
||||
actualSessionID,
|
||||
"fake-refresh-grant-audit-id",
|
||||
actualAuditLog,
|
||||
)
|
||||
|
||||
if test.refreshRequest.want.wantStatus == http.StatusOK {
|
||||
@@ -5141,7 +5195,7 @@ func exchangeAuthcodeForTokens(
|
||||
secrets v1.SecretInterface,
|
||||
oauthStore *storage.KubeStorage,
|
||||
actualAuditLog *bytes.Buffer,
|
||||
sessionID string,
|
||||
actualSessionID string,
|
||||
) {
|
||||
authRequest := deepCopyRequestForm(happyAuthRequest)
|
||||
if test.modifyAuthRequest != nil {
|
||||
@@ -5206,13 +5260,7 @@ func exchangeAuthcodeForTokens(
|
||||
t.Logf("response: %#v", rsp)
|
||||
t.Logf("response body: %q", rsp.Body.String())
|
||||
|
||||
sessionID = getSessionID(t, secrets)
|
||||
|
||||
if test.want.wantAuditLogs != nil {
|
||||
wantAuditLogs := test.want.wantAuditLogs(sessionID)
|
||||
testutil.WantAuditIDOnEveryAuditLog(wantAuditLogs, "fake-code-grant-audit-id")
|
||||
testutil.CompareAuditLogs(t, wantAuditLogs, actualAuditLog.String())
|
||||
}
|
||||
actualSessionID = getSessionID(t, secrets)
|
||||
|
||||
wantNonceValueInIDToken := true // ID tokens returned by the authcode exchange must include the nonce from the auth request (unlike refreshed ID tokens)
|
||||
|
||||
@@ -5226,9 +5274,12 @@ func exchangeAuthcodeForTokens(
|
||||
jwtSigningKey,
|
||||
secrets,
|
||||
approxRequestTime,
|
||||
actualSessionID,
|
||||
"fake-code-grant-audit-id",
|
||||
actualAuditLog,
|
||||
)
|
||||
|
||||
return subject, rsp, authCode, jwtSigningKey, secrets, oauthStore, actualAuditLog, sessionID
|
||||
return subject, rsp, authCode, jwtSigningKey, secrets, oauthStore, actualAuditLog, actualSessionID
|
||||
}
|
||||
|
||||
func getSessionID(t *testing.T, secrets v1.SecretInterface) string {
|
||||
@@ -5255,10 +5306,14 @@ func requireTokenEndpointBehavior(
|
||||
jwtSigningKey *ecdsa.PrivateKey,
|
||||
secrets v1.SecretInterface,
|
||||
requestTime time.Time,
|
||||
actualSessionID string,
|
||||
wantAuditID string,
|
||||
actualAuditLog *bytes.Buffer,
|
||||
) {
|
||||
testutil.RequireEqualContentType(t, tokenEndpointResponse.Header().Get("Content-Type"), "application/json")
|
||||
require.Equal(t, test.wantStatus, tokenEndpointResponse.Code)
|
||||
|
||||
var actualIDToken string
|
||||
if test.wantStatus == http.StatusOK {
|
||||
require.NotNil(t, test.wantSuccessBodyFields, "problem with test table setup: wanted success but did not specify expected response body")
|
||||
|
||||
@@ -5279,7 +5334,7 @@ func requireTokenEndpointBehavior(
|
||||
expectedNumberOfRefreshTokenSessionsStored = 1
|
||||
}
|
||||
if wantIDToken {
|
||||
requireValidIDToken(t, parsedResponseBody, jwtSigningKey, test.wantClientID, wantNonceValueInIDToken, test.wantUsername, test.wantGroups, test.wantAdditionalClaims, test.wantIDTokenLifetimeSeconds, parsedResponseBody["access_token"].(string), requestTime)
|
||||
actualIDToken = requireValidIDToken(t, parsedResponseBody, jwtSigningKey, test.wantClientID, wantNonceValueInIDToken, test.wantUsername, test.wantGroups, test.wantAdditionalClaims, test.wantIDTokenLifetimeSeconds, parsedResponseBody["access_token"].(string), requestTime)
|
||||
}
|
||||
if wantRefreshToken {
|
||||
requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantClientID, test.wantRequestedScopes, test.wantGrantedScopes, test.wantUsername, test.wantGroups, test.wantCustomSessionDataStored, test.wantAdditionalClaims, secrets, requestTime)
|
||||
@@ -5297,6 +5352,12 @@ func requireTokenEndpointBehavior(
|
||||
|
||||
require.JSONEq(t, test.wantErrorResponseBody, tokenEndpointResponse.Body.String())
|
||||
}
|
||||
|
||||
if test.wantAuditLogs != nil {
|
||||
wantAuditLogs := test.wantAuditLogs(actualSessionID, actualIDToken)
|
||||
testutil.WantAuditIDOnEveryAuditLog(wantAuditLogs, wantAuditID)
|
||||
testutil.CompareAuditLogs(t, wantAuditLogs, actualAuditLog.String())
|
||||
}
|
||||
}
|
||||
|
||||
func hashAccessToken(accessToken string) string {
|
||||
@@ -5806,7 +5867,7 @@ func requireValidIDToken(
|
||||
wantIDTokenLifetimeSeconds int,
|
||||
actualAccessToken string,
|
||||
requestTime time.Time,
|
||||
) {
|
||||
) string {
|
||||
t.Helper()
|
||||
|
||||
idToken, ok := body["id_token"]
|
||||
@@ -5891,6 +5952,8 @@ func requireValidIDToken(
|
||||
|
||||
require.NotEmpty(t, actualAccessToken)
|
||||
require.Equal(t, hashAccessToken(actualAccessToken), claims.AccessTokenHash)
|
||||
|
||||
return idTokenString
|
||||
}
|
||||
|
||||
func deepCopyRequestForm(r *http.Request) *http.Request {
|
||||
|
||||
@@ -6,6 +6,7 @@ package credentialrequest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -112,6 +113,14 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Allow cross-referencing the token with the Supervisor's audit logs.
|
||||
r.auditLogger.Audit(auditevent.TokenCredentialRequestTokenReceived, &plog.AuditParams{
|
||||
ReqCtx: ctx,
|
||||
KeysAndValues: []any{
|
||||
"tokenIdentifier", fmt.Sprintf("%x", sha256.Sum256([]byte(credentialRequest.Spec.Token))),
|
||||
},
|
||||
})
|
||||
|
||||
userInfo, err := r.authenticator.AuthenticateTokenCredentialRequest(ctx, credentialRequest)
|
||||
if err != nil {
|
||||
r.auditLogger.Audit(auditevent.TokenCredentialRequestUnexpectedError, &plog.AuditParams{
|
||||
|
||||
@@ -6,6 +6,7 @@ package credentialrequest
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
@@ -67,6 +68,10 @@ func TestNew(t *testing.T) {
|
||||
require.Error(t, err, "the resource panda.bears does not support being converted to a Table")
|
||||
}
|
||||
|
||||
func tokenToHash(tok string) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(tok)))
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
spec.Run(t, "create", func(t *testing.T, when spec.G, it spec.S) {
|
||||
var r *require.Assertions
|
||||
@@ -125,6 +130,10 @@ func TestCreate(t *testing.T) {
|
||||
})
|
||||
|
||||
wantAuditLog = []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("TokenCredentialRequest Token Received", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"tokenIdentifier": tokenToHash(req.Spec.Token),
|
||||
}),
|
||||
testutil.WantAuditLog("TokenCredentialRequest Authenticated User", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"authenticator": map[string]any{
|
||||
@@ -162,6 +171,10 @@ func TestCreate(t *testing.T) {
|
||||
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
|
||||
|
||||
wantAuditLog = []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("TokenCredentialRequest Token Received", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"tokenIdentifier": tokenToHash(req.Spec.Token),
|
||||
}),
|
||||
testutil.WantAuditLog("TokenCredentialRequest Unexpected Error", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"authenticator": map[string]any{
|
||||
@@ -188,6 +201,10 @@ func TestCreate(t *testing.T) {
|
||||
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
|
||||
|
||||
wantAuditLog = []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("TokenCredentialRequest Token Received", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"tokenIdentifier": tokenToHash(req.Spec.Token),
|
||||
}),
|
||||
testutil.WantAuditLog("TokenCredentialRequest Authentication Failed", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"authenticator": map[string]any{
|
||||
@@ -214,6 +231,10 @@ func TestCreate(t *testing.T) {
|
||||
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
|
||||
|
||||
wantAuditLog = []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("TokenCredentialRequest Token Received", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"tokenIdentifier": tokenToHash(req.Spec.Token),
|
||||
}),
|
||||
testutil.WantAuditLog("TokenCredentialRequest Unexpected Error", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"authenticator": map[string]any{
|
||||
@@ -241,6 +262,10 @@ func TestCreate(t *testing.T) {
|
||||
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
|
||||
|
||||
wantAuditLog = []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("TokenCredentialRequest Token Received", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"tokenIdentifier": tokenToHash(req.Spec.Token),
|
||||
}),
|
||||
testutil.WantAuditLog("TokenCredentialRequest Unsupported UserInfo", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"authenticator": map[string]any{
|
||||
@@ -277,6 +302,10 @@ func TestCreate(t *testing.T) {
|
||||
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
|
||||
|
||||
wantAuditLog = []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("TokenCredentialRequest Token Received", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"tokenIdentifier": tokenToHash(req.Spec.Token),
|
||||
}),
|
||||
testutil.WantAuditLog("TokenCredentialRequest Unsupported UserInfo", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"authenticator": map[string]any{
|
||||
@@ -313,6 +342,10 @@ func TestCreate(t *testing.T) {
|
||||
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
|
||||
|
||||
wantAuditLog = []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("TokenCredentialRequest Token Received", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"tokenIdentifier": tokenToHash(req.Spec.Token),
|
||||
}),
|
||||
testutil.WantAuditLog("TokenCredentialRequest Unsupported UserInfo", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"authenticator": map[string]any{
|
||||
@@ -389,6 +422,10 @@ func TestCreate(t *testing.T) {
|
||||
r.NotEmpty(response)
|
||||
|
||||
wantAuditLog = []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("TokenCredentialRequest Token Received", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"tokenIdentifier": tokenToHash(req.Spec.Token),
|
||||
}),
|
||||
testutil.WantAuditLog("TokenCredentialRequest Authenticated User", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"authenticator": map[string]any{
|
||||
@@ -436,6 +473,10 @@ func TestCreate(t *testing.T) {
|
||||
r.Empty(validationFunctionSawTokenValue)
|
||||
|
||||
wantAuditLog = []testutil.WantedAuditLog{
|
||||
testutil.WantAuditLog("TokenCredentialRequest Token Received", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"tokenIdentifier": tokenToHash(req.Spec.Token),
|
||||
}),
|
||||
testutil.WantAuditLog("TokenCredentialRequest Authenticated User", map[string]any{
|
||||
"auditID": "fake-audit-id",
|
||||
"authenticator": map[string]any{
|
||||
|
||||
@@ -92,6 +92,9 @@ correlate an audit event log line to other logs. The values for these keys are o
|
||||
- When applicable, audit logs have an `authorizeID` which is a unique ID to allow audit events to be correlated
|
||||
across some of the browser redirects which relate to a single login attempt by an end user. This is only applicable
|
||||
to those browser-based login flows which use redirects to identity providers and/or interstitial pages in the login flow.
|
||||
- When applicable, audit logs have a `tokenIdentifier` which is a unique ID of a token to allow audit events to be correlated
|
||||
between where a token is issued to an end user in the Supervisor and where a token is used to gain access to a
|
||||
Kubernetes cluster in the Concierge.
|
||||
|
||||
Each audit event may also have more key-value pairs specific to the event's type.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user