Skip to content
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
13 changes: 2 additions & 11 deletions api/session-manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ paths:
- generate:
- a state ID for the state parameter of the OIDC authorize endpoint
- a PKCE verifier and the related PKCE challenge (S256)
- create a fingerprint according to the selected method (see below)
- based on the state ID, persist the PKCE verifier, fingerprint, CMK tenant ID and the given request URI
- based on the state ID, persist the PKCE verifier, CMK tenant ID and the given request URI
- build the authorize URI and return it with a 302 response.

With this the CMK UI initiated the flow and the browser is redirected to the correct OIDC provider. Based on the embedded redirect_uri the Session Manager will directly receive the auth code from the OIDC provider on the /callback endpoint to continue the flow.
Expand Down Expand Up @@ -145,16 +144,14 @@ paths:
get:
description: |
After creating the auth code, the OIDC provider calls this endpoint, delivering the code. The Session Manager can then requests the OIDC provider to exchange the code for a valid token, additionally providing the related PKCE verifier. Steps:
- for the given state ID, look up the request URI, PKCE verifier, fingerprint,
- for the given state ID, look up the request URI, PKCE verifier,
CMK tenant ID and token endpoint from the storage
- verify the current fingerprint matches the one from the storage
- perform a POST request to the token endpoint
- based on the returned OIDC token, generate a session ID and CSRF token and store:
- the session ID
- the CSRF token
- the CMK tenant ID
- parts of the the OIDC token: (claims, access token, refresh token)
- the fingerprint
- the date and time the session shall expire
- remove the state for the state ID from the storage
- in the response set the session and CSRF cookies
Expand Down Expand Up @@ -189,12 +186,6 @@ paths:
Location:
schema:
type: string
"403":
description: Fingerprint mismatch (returned when error_uri is not available)
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorModel"
default:
description: Unexpected error (returned when error_uri is not available)
content:
Expand Down
6 changes: 1 addition & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/moby/moby/api v1.54.2
github.com/oapi-codegen/runtime v1.4.1
github.com/openkcm/api-sdk v0.18.0
github.com/openkcm/api-sdk v0.18.1
github.com/openkcm/common-sdk v1.16.1
github.com/pressly/goose/v3 v3.27.1
github.com/samber/oops v1.22.0
Expand Down Expand Up @@ -51,7 +51,6 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
Expand All @@ -66,8 +65,6 @@ require (
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
Expand Down Expand Up @@ -124,7 +121,6 @@ require (
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
github.com/pingcap/log v1.1.0 // indirect
github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
Expand Down
12 changes: 2 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
Expand Down Expand Up @@ -68,10 +66,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/exaring/otelpgx v0.11.1 h1:pE79fIg/qh/Lpu00kvswFC5dKfqyJJhMJ4Y4N3w5Lj4=
github.com/exaring/otelpgx v0.11.1/go.mod h1:3OojrUKhhy3lTbYIMBijP3YjMey/jo14eHAW5cXcUdk=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
Expand Down Expand Up @@ -254,8 +248,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/openkcm/api-sdk v0.18.0 h1:bLURye5GkxSM9SqT3ji94A9HgI9/KgHKFTNgnIeSRWk=
github.com/openkcm/api-sdk v0.18.0/go.mod h1:3CQUZVNl/Nu5K71M1arSiTsJhyLloO3pJpqwBL1oE1o=
github.com/openkcm/api-sdk v0.18.1 h1:Ch8iPTKz/PAgHI1HVHeYHKT8iuKjb5jHHRJoPl10HiM=
github.com/openkcm/api-sdk v0.18.1/go.mod h1:3CQUZVNl/Nu5K71M1arSiTsJhyLloO3pJpqwBL1oE1o=
github.com/openkcm/common-sdk v1.16.1 h1:0GIFrEDR7qXj/ghSvwWVHnWCAzBGD7iFbdRN4WWv9tM=
github.com/openkcm/common-sdk v1.16.1/go.mod h1:2UuIjhOuee6gQKJt3EnenhB8mXWriGHEXdxCGPWfRxw=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
Expand All @@ -275,8 +269,6 @@ github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoO
github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 h1:W3rpAI3bubR6VWOcwxDIG0Gz9G5rl5b3SL116T0vBt0=
github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0/go.mod h1:+8feuexTKcXHZF/dkDfvCwEyBAmgb4paFc3/WeYV2eE=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
Expand Down
49 changes: 8 additions & 41 deletions integration/session_grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ func TestSessionGRPC(t *testing.T) {

t.Run("GetSession - session not found", func(t *testing.T) {
resp, err := sessionClient.GetSession(ctx, &sessionv1.GetSessionRequest{
SessionId: "non-existent-session",
TenantId: "tenant-123",
Fingerprint: "fingerprint-123",
SessionId: "non-existent-session",
TenantId: "tenant-123",
})
assert.NoError(t, err)
assert.NotNil(t, resp)
Expand All @@ -58,7 +57,6 @@ func TestSessionGRPC(t *testing.T) {
sess := session.Session{
ID: uuid.Must(uuid.NewV4()).String(),
TenantID: "tenant-inactive",
Fingerprint: "fingerprint-inactive",
Issuer: "https://issuer.example.com",
ProviderID: "provider-123",
AccessToken: "token-123",
Expand All @@ -71,9 +69,8 @@ func TestSessionGRPC(t *testing.T) {
require.NoError(t, err)

resp, err := sessionClient.GetSession(ctx, &sessionv1.GetSessionRequest{
SessionId: sess.ID,
TenantId: sess.TenantID,
Fingerprint: sess.Fingerprint,
SessionId: sess.ID,
TenantId: sess.TenantID,
})
assert.NoError(t, err)
assert.NotNil(t, resp)
Expand All @@ -85,7 +82,6 @@ func TestSessionGRPC(t *testing.T) {
sess := session.Session{
ID: uuid.Must(uuid.NewV4()).String(),
TenantID: "tenant-active",
Fingerprint: "fingerprint-active",
Issuer: "https://issuer.example.com",
ProviderID: "provider-active",
AccessToken: "token-active",
Expand All @@ -109,47 +105,19 @@ func TestSessionGRPC(t *testing.T) {
// Note: This test will fail validation because there's no trust mapping configured
// but it tests the session retrieval path
resp, err := sessionClient.GetSession(ctx, &sessionv1.GetSessionRequest{
SessionId: sess.ID,
TenantId: sess.TenantID,
Fingerprint: sess.Fingerprint,
SessionId: sess.ID,
TenantId: sess.TenantID,
})
assert.NoError(t, err)
assert.NotNil(t, resp)
// Will be false because trust mapping is not configured, but tests the flow
assert.False(t, resp.GetValid())
})

t.Run("GetSession - fingerprint mismatch", func(t *testing.T) {
sess := session.Session{
ID: uuid.Must(uuid.NewV4()).String(),
TenantID: "tenant-fingerprint",
Fingerprint: "correct-fingerprint",
Issuer: "https://issuer.example.com",
ProviderID: "provider-fp",
AccessToken: "token-fp",
Expiry: time.Now().Add(1 * time.Hour),
}
err := sessionRepo.StoreSession(ctx, sess)
require.NoError(t, err)

err = sessionRepo.BumpActive(ctx, sess.ID, 1*time.Hour)
require.NoError(t, err)

resp, err := sessionClient.GetSession(ctx, &sessionv1.GetSessionRequest{
SessionId: sess.ID,
TenantId: sess.TenantID,
Fingerprint: "wrong-fingerprint",
})
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.False(t, resp.GetValid())
})

t.Run("GetSession - tenant ID mismatch", func(t *testing.T) {
sess := session.Session{
ID: uuid.Must(uuid.NewV4()).String(),
TenantID: "correct-tenant",
Fingerprint: "fingerprint-tenant",
Issuer: "https://issuer.example.com",
ProviderID: "provider-tenant",
AccessToken: "token-tenant",
Expand All @@ -162,9 +130,8 @@ func TestSessionGRPC(t *testing.T) {
require.NoError(t, err)

resp, err := sessionClient.GetSession(ctx, &sessionv1.GetSessionRequest{
SessionId: sess.ID,
TenantId: "wrong-tenant",
Fingerprint: sess.Fingerprint,
SessionId: sess.ID,
TenantId: "wrong-tenant",
})
assert.NoError(t, err)
assert.NotNil(t, resp)
Expand Down
4 changes: 1 addition & 3 deletions internal/business/server/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import (
"net/http"
"strings"

"github.com/openkcm/common-sdk/pkg/fingerprint"

commonmiddleware "github.com/openkcm/common-sdk/pkg/middleware"
slogctx "github.com/veqryn/slog-context"

Expand All @@ -35,7 +33,7 @@ func createHTTPServer(_ context.Context, cfg *config.Config, sManager *session.M
},
)

handler := fingerprint.FingerprintCtxMiddleware(openapi.Handler(strictHandler))
handler := openapi.Handler(strictHandler)
handler = middleware.ResponseWriterMiddleware(handler)
handler = commonmiddleware.SecurityHeadersMiddleware(handler, map[string]string{
"Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';",
Expand Down
30 changes: 6 additions & 24 deletions internal/business/server/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"strings"

"github.com/openkcm/common-sdk/pkg/csrf"
"github.com/openkcm/common-sdk/pkg/fingerprint"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"

Expand All @@ -24,8 +23,8 @@ import (
// sessionManager defines the interface for session management operations
// used by the OpenAPI server.
type sessionManager interface {
MakeAuthURI(ctx context.Context, tenantID, fingerprint, requestURI, errorURI string) (string, string, error)
FinaliseOIDCLogin(ctx context.Context, state, code, fingerprint string) (session.OIDCSessionData, error)
MakeAuthURI(ctx context.Context, tenantID, requestURI, errorURI string) (string, string, error)
FinaliseOIDCLogin(ctx context.Context, state, code string) (session.OIDCSessionData, error)
MakeSessionCookie(ctx context.Context, tenantID, sessionID string) (*http.Cookie, error)
MakeCSRFCookie(ctx context.Context, tenantID, csrfToken string) (*http.Cookie, error)
MakeLoginCSRFCookie(ctx context.Context, csrfToken string) (*http.Cookie, error)
Expand Down Expand Up @@ -88,15 +87,7 @@ func (s *openAPIServer) Auth(ctx context.Context, request openapi.AuthRequestObj
return s.authErrorResponse(errorURI, serviceerr.ErrInvalidRequest), nil
}

fingerprint, err := fingerprint.ExtractFingerprint(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to extract fingerprint")
slogctx.Error(ctx, "Failed to extract fingerprint", "error", err)
return s.authErrorResponse(errorURI, serviceerr.ErrUnknown), nil
}

url, csrfToken, err := s.sManager.MakeAuthURI(ctx, request.Params.TenantID, fingerprint, request.Params.RequestURI, errorURI)
url, csrfToken, err := s.sManager.MakeAuthURI(ctx, request.Params.TenantID, request.Params.RequestURI, errorURI)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to build auth URI")
Expand Down Expand Up @@ -146,14 +137,6 @@ func (s *openAPIServer) Callback(ctx context.Context, req openapi.CallbackReques
// Try to load error_uri from state (best-effort, for error redirect)
errorURI := s.getErrorURIFromState(ctx, req.Params.State)

currentFingerprint, err := fingerprint.ExtractFingerprint(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed extract fingerprint")
slogctx.Error(ctx, "Failed to extract fingerprint", "error", err)
return s.callbackErrorResponse(errorURI, serviceerr.ErrUnknown), nil
}

// Get the response writer from the context
rw, err := middleware.ResponseWriterFromContext(ctx)
if err != nil {
Expand All @@ -170,7 +153,7 @@ func (s *openAPIServer) Callback(ctx context.Context, req openapi.CallbackReques
return s.callbackErrorResponse(errorURI, err), nil
}

result, err := s.sManager.FinaliseOIDCLogin(ctx, req.Params.State, req.Params.Code, currentFingerprint)
result, err := s.sManager.FinaliseOIDCLogin(ctx, req.Params.State, req.Params.Code)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to finalise OIDC login")
Expand Down Expand Up @@ -227,7 +210,7 @@ func (s *openAPIServer) callbackErrorResponse(errorURI string, err error) openap
}

// callbackFinaliseErrorResponse handles the error case after FinaliseOIDCLogin fails,
// masking fingerprint mismatch details when no error redirect is available.
// masking sensitive details when no error redirect is available.
func (s *openAPIServer) callbackFinaliseErrorResponse(errorURI string, err error) openapi.CallbackResponseObject {
if redirectURL := s.buildErrorRedirectURL(errorURI, err); redirectURL != "" {
return openapi.Callback302Response{
Expand All @@ -237,8 +220,7 @@ func (s *openAPIServer) callbackFinaliseErrorResponse(errorURI string, err error

body, status := s.toErrorModel(err)
if status == 403 {
// return generic Unauthorized for 403 Forbidden to avoid leaking information on
// fingerprint mismatch in the original error body
// return generic Unauthorized for 403 Forbidden to avoid leaking sensitive information
body, status = s.toErrorModel(serviceerr.ErrUnauthorized)
}
return openapi.CallbackdefaultJSONResponse{
Expand Down
Loading
Loading