@@ -73,9 +73,10 @@ Feature: override-accounts-account-properties-claims per-request + JWT claims de
7373 * match payload.password == '#notpresent'
7474 * match payload.emailVerificationToken == '#notpresent'
7575
76- # The override REPLACES the static list — "teams" (from the static default) must
77- # not leak in either, since it was not requested in this override.
78- * match payload.teams == '#notpresent'
76+ # The override REPLACES the static list, but it cannot lower the required-claims floor:
77+ # "teams" is listed in jwtConfigProvider/required-account-properties-claims, so it is
78+ # present even though this override never asked for it. See the dedicated scenario below.
79+ * match payload.teams == '#present'
7980
8081 # ---------------------------------------------------------------------------
8182 Scenario : /token applies the same per-request override and denylist
@@ -109,5 +110,117 @@ Feature: override-accounts-account-properties-claims per-request + JWT claims de
109110 * match payload.password == '#notpresent'
110111 * match payload.emailVerificationToken == '#notpresent'
111112
112- # Override REPLACES the static list — default claims must not leak
113- * match payload.teams == '#notpresent'
113+ # Override REPLACES the static list, but required claims survive it — see the
114+ # dedicated scenario below.
115+ * match payload.teams == '#present'
116+
117+ # ---------------------------------------------------------------------------
118+ Scenario : /token resolves claims from a MongoRealmAccount, not just a file-realm one
119+ # ---------------------------------------------------------------------------
120+ # Regression test. The two scenarios above cover /auth/verify (a user document read
121+ # from MongoDB) and /token (a file-realm user). The combination left untested —
122+ # /token with a *MongoRealmAccount* — was broken for a whole release: claim selection
123+ # went through a JXPath helper meant for the YAML configuration tree, which resolved
124+ # against the plain map a FileRealmAccount hands back but not against the map
125+ # MongoRealmAccount rebuilds through GSON. It failed silently, since that helper
126+ # swallows every error and returns null.
127+ * def email = 'claims-mongo-' + java.util.UUID.randomUUID() + '@example.com'
128+ * def password = 'Password123!'
129+
130+ Given path '/auth/register'
131+ And request
132+ """
133+ {
134+ "firstName": "Mongo",
135+ "lastName": "Claims",
136+ "teamName": "Mongo Claims Co",
137+ "email": "#(email)",
138+ "password": "#(password)"
139+ }
140+ """
141+ When method POST
142+ Then status 201
143+
144+ Given path '/users/' + email
145+ And header Authorization = adminAuth
146+ When method GET
147+ Then status 200
148+ * def verificationToken = response.emailVerificationToken
149+
150+ Given path '/auth/verify'
151+ And param email = email
152+ And param token = verificationToken
153+ When method GET
154+ Then status 302
155+
156+ # Give the user a nested profile field to select as a claim
157+ Given path '/users/' + email
158+ And header Authorization = adminAuth
159+ And request { "profile" : { "name" : "MongoClaims" } }
160+ When method PATCH
161+ Then status 200
162+
163+ # Authenticate with credentials: the account is a MongoRealmAccount, so its properties
164+ # are the user document — the case that used to yield no claims at all.
165+ * def Base64 = Java.type('java.util.Base64' )
166+ * def encoded = Base64.getEncoder().encodeToString((email + ':' + password).getBytes())
167+
168+ Given path '/token'
169+ And header Authorization = 'Basic ' + encoded
170+ And param _claims-override = 'profile'
171+ When method POST
172+ Then status 200
173+
174+ * def parts = response.access_token.split('.' )
175+ * def payload = JSON.parse(new java.lang.String(java.util.Base64.getUrlDecoder().decode(parts[1])))
176+ * karate.log('JWT payload from /token (mongo realm):' , payload)
177+
178+ * match payload.profile == '#present'
179+ * match payload.profile.name == 'MongoClaims'
180+
181+ # ---------------------------------------------------------------------------
182+ Scenario : required claims survive an override that does not list them
183+ # ---------------------------------------------------------------------------
184+ # required-account-properties-claims is the floor a per-request override cannot lower.
185+ # It exists because a deployment can depend on a claim being there — a multi-tenant node
186+ # verifies the claim naming the issuing node on every later request, so a tenant able to
187+ # drop it would lock itself out with a config change.
188+ #
189+ # conf-overrides.yml sets jwtConfigProvider/required-account-properties-claims to include
190+ # "teams"; the override below asks for "profile" only.
191+ * def creds = 'claimsTest:ClaimsPass123!'
192+ * def Base64 = Java.type('java.util.Base64' )
193+ * def encoded = Base64.getEncoder().encodeToString(creds.getBytes())
194+
195+ Given path '/token'
196+ And header Authorization = 'Basic ' + encoded
197+ And param _claims-override = 'profile'
198+ When method POST
199+ Then status 200
200+
201+ * def parts = response.access_token.split('.' )
202+ * def payload = JSON.parse(new java.lang.String(java.util.Base64.getUrlDecoder().decode(parts[1])))
203+ * karate.log('JWT payload with required claims:' , payload)
204+
205+ # The override was applied ...
206+ * match payload.profile == '#present'
207+ # ... and the required claim is there anyway, though the override never listed it
208+ * match payload.teams == '#present'
209+
210+ # ---------------------------------------------------------------------------
211+ Scenario : the denylist wins over required claims
212+ # ---------------------------------------------------------------------------
213+ # A credential must not become a claim even if configuration declares it required.
214+ * def creds = 'claimsTest:ClaimsPass123!'
215+ * def Base64 = Java.type('java.util.Base64' )
216+ * def encoded = Base64.getEncoder().encodeToString(creds.getBytes())
217+
218+ Given path '/token'
219+ And header Authorization = 'Basic ' + encoded
220+ And param _claims-override = 'password'
221+ When method POST
222+ Then status 200
223+
224+ * def parts = response.access_token.split('.' )
225+ * def payload = JSON.parse(new java.lang.String(java.util.Base64.getUrlDecoder().decode(parts[1])))
226+ * match payload.password == '#notpresent'
0 commit comments